Item 37 : ordinal 인덱싱 대신 EnumMap을 사용하라
우리는 Item 35에서 ordinal을 통해 enum의 원소의 위치값을 구할 수 있다는 것을 배웠다.
그래서 만약 배열이나 리스트에서 원소를 꺼낼 때 ordinal 메소드로 얻는 코드가 존재할 수도 있다.
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;
}
}
다음과 같은 식물 class가 있을 때 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]);
}
문제가 가득한 코드이다.
배열은 제네릭과 호환되지 않으니 비검사 형변환이 수행되어야 하며
배열은 각 인덱스의 의미를 모르니 출력 결과에 직접 레이블을 달아야한다.(.values())
가장 심각한 문제는 정확한 정숫값을 사용한다는 것을 사용자가 직접 보증해야한다는 점이다.
이러한 문제점을 해결하기 위해 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);
}
이렇게 만들면 안전하지 않은 형변환은 쓰지 않고,
맵의 키인 열거 타입이 그 자체로 출력용 문자열을제공하니 출력 결과에 직접 레이블을 달 일도 없다.
나아가 배열 인덱스를 계산하는 과정에서 오류가 날 가능성도 원천봉쇄된다.
EnumMap은 내부에서 배열을 사용하는데 내부 구현 방식을 안으로 숨겨서 Map의 타입 안정성과 배열의 성능을 모두 얻어냈다.
여기서 스트림을 사용하면 더 짧게 줄일수있다.
//EnumMap을 사용하지 않음
Arrays.stream(garden).collect(groupingBy(p -> p.lifeCycle))
//EnumMap 사용
Arrays.stream(garden)
.collect(groupingBy(p -> p.lifeCycle, () -> new EnumMap<>(LifeCycle.class),
toSet()));
EnumMap만 사용했을 때와 달리 살짝 다르게 작동한다.
EnumMap버전은 언제나 식물의 생애주기당 하나씩의 중첩 맵을 만들지만
스트림버전에서는 해당 생애주기에 속하는 식물이 있을 때만 만든다.
이제는 ordianl()을 사용한 경우와 EnumMap을 사용했을 때 데이터를 수정할 경우가 생기는 상황을 봐보자
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()];
}
}
}
기체 액체 고체의 상태변화에 대한 코드인데
컴파일러는 ordinal과 배열 인덱스의 고나계를 알 도리가 없기 때문에 자칫하면 런타임 오류가 날 가능성이 많다.
거기에 표의 크기가 늘어날수록 null로 채워지는 칸도 늘어날 것이다.
EnumMap으로 만든 코드를 보자.
public enum Phase {
SOLID, LIQUID, GAS;
public enum Transition {
MELT(SOLID, LIQUID),
FREEZE(LIQUID, SOLID),
BOIL(LIQUID, GAS),
CONDENSE(GAS, SOLID),
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 static final Map<Phase, Map<Phase, Transition>> m = Stream.of(values())
.collect(groupingBy(t -> t.from, () -> new EnumMap<>(Phase.class),
toMap(t -> t.to, //key-mapper
t -> t, //value-mapper
(x, y) -> y, //merge-function
() -> new EnumMap<>(Phase.class))));
public static Transition from (Phase from, Phase to) {
return m.get(from).get(to);
}
}
}
from과 to를 매핑할때 불필요한 코드들이 사라지며 간결해진다.
새로운 상태인 PLASMA가 추가되더라도
IONIZE(GAS, PLASMA),DEIONIZE(PLASMA, GAS);
이런식으로 간결하게 상태 목록을 추가할 수 있다.
'책 > 이펙티브자바' 카테고리의 다른 글
Item 39 : 명명 패턴보다 어노테이션을 사용하라 (0) | 2021.08.07 |
---|---|
Item 38 : 확장할 수 있는 열거 타입이 필요하면 인터페이스를 사용하라 (0) | 2021.08.06 |
Item 36 : 비트 필드 대신 EnumSet을 사용하라 (0) | 2021.08.05 |
Item 35 : ordinal 메소드 대신 인스턴스 필드를 사용하라 (0) | 2021.08.03 |
Item 34 : int 상수 대신 열거 타입을 사용하라 (0) | 2021.08.02 |