[Why]
배열이나 리스트에서 원소를 꺼낼 때 ordinal 메서드(아이템35)로 인덱스를 얻는 코드가 있다.
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<>();
}
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]);
}
동작은 하지만 문제가 한가득이다.
- 배열은 제네릭과 호환되지 않으니(아이템28) 비검사 형변환을 수행해야 하고 깔끔히 컴파일되지 않을 것이다.
- 배열은 각 인덱의 의미를 모르니 출력 결과에 직접 레이블을 달아야 한다.
- 가장 심각한 문제는 정확한 정숫값을 사용한다는 것을 직접 보증해야 한다는 점이다. 정수는 타입 안전하지 않다.
[How]
예제에서 배열은 실질적으로 열거 타입 상수를 값으로 매핑하는 일을 한다. 그러니 Map을 사용할 수도 있다.
- 열거 타입을 키로 사용하도록 설계한 아주 빠른 Map 구현체가 존재하는데, 바로 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);
}
System.out.println(plantsByLifeCycle);
- 더 짧고 명료하고 안전하고 성능도 원래 버전과 비등하다.
- 맵의 키인 열거 타입이 그 자체로 출력용 문자열을 제공하니 출력 결과에 직접 레이블을 달 일도 없다.
- EnumMap의 생성자가 받는 키 타입의 Class 객체는 한정적 타입 토큰으로, 런타임 제네릭 타입 정보를 제공한다(아이템33).
System.out.println(Arrays.stream(garden)
.collect(groupingBy(p -> p.lifeCycle,
() -> new EnumMap<>(LifeCycle.class), toSet())));
- 스트림(아이템45)을 사용해 맵을 관리하면 코드를 더 줄일 수 있다.
- 스트림을 사용하면 EnumMap만 사용했을 때와는 살짝 다르게 동작한다.
- EnumMap 버전은 언제나 식물의 생애주기당 하나씩의 중첩을 만들지만, 스트림 버전에서는 해당 생애주기에 속하는 식물이 있을 때만 만든다.
- 예컨대 정원에 한해살이와 여러해살이 식물만 살고 두해살이는 없다면, EnumMap 버전에서는 맵을 3개 만들고 스트림 버전에서는 2개만 만든다.
두 열거 타입 값들을 매핑하느라 ordinal을 두 번이나 쓴 배열들의 배열
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()];
}
}
}
- 위의 정원과 같은 문제가 있다. EnumMap을 사용하는 편이 훨씬 낫다.
- 전이 하나를 얻으려면 이전 상태(from)와 이후 상태(to)가 필요하니, 맵 2개를 중첩하면 쉽게 해결할 수 있다.
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;
}
// 상전이 맵을 초기화한다.
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);
}
}
}
- 상전이(phase transition) 맵을 초기화하는 코드는 제법 복잡하다.
- Map<Phase, Map<Phase, Transition>>은 "이전 상태에서 '이후 상태에서 전이로의 맵'에 대응시키는 맵"이라는 뜻이다.
- 이러한 맵의 맵을 초기화하기 위해 수집기(java.util.stream.Collector) 2개를 차례로 사용했다.
- 첫 번째 수집기인 toMap에서는 이후 상태를 전이에 대응시키는 EnumMap을 생성한다.
- 두번째 수집기의 병합 함수인 (x, y) -> y는 선언만 하고 실제로는 쓰이지 않는데, 이는 단지 EnumMap을 얻으려면 맵 팩터리가 필요하고 수집기들은 점층적 팩터리(telescoping factory)를 제공하기 때문이다.
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);
... // 나머지 코드는 그대로다.
}
}
- 배열로 만든 코드를 수정하려면 새로운 상수를 Phase에 1개, Phase.Transition에 2개를 추가하고, 원소 9개짜리인 배열들의 배열을 원소 16개짜리로 교체해야 한다.
- 반면, EnumMap 버전에서는 상태 목록에 PLASMA를 추가하고, 전이 목록에 IONIZE(GAS, PLASMA)와 DEIONIZE(PLASMA, GAS)만 추가하면 끝이다.
'독서찰기(讀書札記) > 이펙티브 자바' 카테고리의 다른 글
[아이템 43] 람다보다는 메서드 참조를 사용하라 (0) | 2022.03.03 |
---|---|
[아이템 42] 익명 클래스보다는 람다를 사용하라 (0) | 2022.03.02 |
[아이템 36] 비트 필드 대신 EnumSet을 사용하라 (0) | 2022.02.20 |
[아이템 35] ordinal 메서드 대신 인스턴스 필드를 사용하라 (0) | 2022.02.20 |
[아이템 34] int 상수 대신 열거 타입을 사용하라 (0) | 2022.02.18 |