[Effective Java] 6 - item 37 ordinal
인덱싱 대신 EnumMap
을 사용하라.
ordinal
인덱싱
간혹 배열 혹은 리스트에서 원소를 꺼낼 때 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;
}
}
Set<Plant>[] plantsByLifeCycle = (Set<Plant>[]) new Set[Plant.Lifecycle.values().length]; // 형변환
for (int i = 0 ; i < plantsByLifeCycle.length ; i++) {
plantsByLifeCycle[i] = new HashSet<>();
}
List<Plant> garden = Arrays.asList(
new Plant("ANNUAL_TREE_1", Plant.Lifecycle.ANNUAL),
new Plant("ANNUAL_TREE_2", Plant.Lifecycle.ANNUAL),
new Plant("ANNUAL_TREE_3", Plant.Lifecycle.ANNUAL),
new Plant("BIENNIAL_TREE_1", Plant.Lifecycle.BIENNIAL),
new Plant("PERENNIAL_TREE_1", Plant.Lifecycle.PERENNIAL)
);
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]); // 직접 작성해 주어야 한다.
}
- 배열은 제네릭과 호환되지 않으므로, 비검사 현변환을 수행해야 하며 깔끔하게 컴파일되지 않을 것이다.
- 배열은 각 인덱스의 의미를 모르니 출력 결과에 직접 레이블을 달아야 한다.
- 정확한 값을 사용한다는 것을 클라이언트 코드 작성자가 직접 보증해야 한다. (not type-safe)
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);
}
toString
이 적절하게 구현되어 있으므로 별도 포매팅이 필요없다.- 로직이 더욱 간단해졌다.
- 안전하지 않은 형변환을 쓰지 않는다.
- 배열 인덱스 계산 과정에서 오류 가능성도 사라졌다.
Set[]
을 사용하여 관리하는 것보다EnumMap
을 사용하는 것이 간결하고 성능도 비슷하다.EnumMap
의 성능이ordinal
을 쓴 배열에 비견되는 이유는 그 내부에서 배열을 사용하기 때문이다. 구현을 내부로 숨겨 Map의 타입 안전성과 배열의 성능을 모두 얻어낸 것이다.
EnumMap
의 생성자가 받는 키 타입의 Class 객체는 한정적 타입 토큰으로, 런타임 제네릭 타입 정보를 제공한다. item 33
Stream
을 사용한 코드 개선
Map<Plant.Lifecycle, List<Plant>> newGarden =
garden.stream()
// ANNUAL -> BIENNIAL -> PERENNIAL 순으로 확인
.sorted((o1, o2) -> o2.lifeCycle.ordinal() - o1.lifeCycle.ordinal())
.collect(groupingBy(p -> p.lifeCycle));
System.out.println(newGarden);
- 이 코드는 EnumMap이 아닌 고유한 맵 구현체를 사용하기 때문에 EnumMap을 써서 얻은 공간과 성능 이점이 사라진다는 문제가 발생한다.
EnumMap<Plant.Lifecycle, Set<Plant>> newgarden = garden.stream()
.collect(groupingBy(
p -> p.lifeCycle,
() -> new EnumMap<>(Plant.Lifecycle.class), toSet()));
// mapFactory 매개변수에 원하는 구현체 명시해 호출한다.
System.out.println(newgarden);
- 매개변수 3개짜리
Collections.groupingBy
메서드는mapFactory
매개변수에 원하는 맵 구현체를 명시해 호출할 수 있다. - 위 코드는 Map을 빈번하게 사용하는 프로그램에서 최적화하는 방법이다.
// Collectors의 groupingBy 메서드
public static <T, K, D, A, M extends Map<K, D>>
Collector<T, ?, M> groupingBy(Function<? super T, ? extends K> classifier,
Supplier<M> mapFactory,
Collector<? super T, A, D> downstream)
중첩 EnumMap
public enum Phase {
SOLID,
LIQUID,
GAS;
public enum Transition {
MELT,
FREEZE,
BOIL,
CONDENSE,
SUBLIME,
DEPOSIT;
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을 두 번이나 쓴 잘못된 방법의 예시이다.
- 컴파일러가 ordinal과 배열 인덱스의 관계를 알 수 없다.
Phase
나Transition
열거 타입을 수정하면서 배열TRANSITIONS
를 함께 수정하지 않거나 실수로 잘못 수정하면 런타임 오류가 발생할 것이다.ArrayIndexOutOfBoundsException
이나NullPointerException
을 던질 수도 있고, 예외도 발생시키지 않고 이상하게 동작할 수 있다.- 상전이 표(배열)의 크기는 상태의 가짓수가 늘어나면 제곱해서 커지며,
null
로 채워지는 칸도 늘어날 것이다.
이렇게 사용하지 말고 중첩된 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;
}
// 상전이 맵 초기화 (이전 상태에서 이후 상태 & 전이 InnerMap 에 대응시키는 맵)
private static final Map<Phase, Map<Phase, Transition>> m =
Stream.of(values()) // enum 타입 두 개를 매핑한 필드 리스트
.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);
}
}
}
위와 같이 중첩 EnumMap
을 사용하는 경우 2차원 배열로 선언하였을 때보다 새로운 상태를 추가하기도 용이할 것이다.
public enum Phase {
SOLID, LIQUID, GAS,
// 신규 PLASMA 추가
PLASMA;
public enum Transition {
MELT(SOLID, LIQUID), FREEZE(LIQUID, SOLID),
BOIL(LIQUID, GAS), CONDENSE(GAS, LIQUID),
SUBLIME(SOLID, GAS), DEPOSIT(GAS, SOLID),
// IONIZE, DEIONIZE 추가
IONIZE(GAS, PLASMA), DEIONIZE(PLASMA, GAS);
...
}
상태 목록에 PLASMA
를 추가하고, 전이 목록에 IONIZE(GAS, PLASMA)
와 DEIONIZE(PLASMA, GAS)
만 추가하면 끝이다. 반면 이전 코드에서 새로운 상수를 추가할 경우, 상수 3가지 뿐만 아니라 - 원소 9개 짜리인 2차원 배열을 원소 16개로 교체해야 한다. 런타임에 문제가 생길 가능성 또한 높다.
Leave a comment