[Effective Java] item 37 : ordinal 인덱싱 대신 EnumMap을 사용하라

DEINGVELOP·2023년 2월 21일
0

Effective Java

목록 보기
13/19

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]);
}
  • 배열은 제네릭과 호환되지 않아 비검사 형변환을 수행해야 함 -> 깔끔하게 컴파일되지 않음

  • 배열은 각 인덱스의 의미를 모르니 출력 결과에 직접 레이블을 달아야 함

  • 정확한 정숫값을 사용한다는 것을 직접 프로그래머가 보증해야 함. 정수는 열거 타입과 달리 타입 안전하지 않기 때문

Map 사용

  • EnumMap : 열거타입을 키로 사용하도록 설계한 아주 빠른 Map 구현체
Map<Plant.LifeCycle, Set<Plant» plantsByLifeCycle =
	new EnumMapo(Plant.LifeCycle.class);
for (Plant.LifeCycle lc : Plant.LifeCycle.values())
	plantsByLifeCycle. put (Ic, new HashSetoO);
for (Plant p : garden)
	plantsByLifeCycle.get(p.lifeCycle).add(p);
System.out.printIn(plantsByLifeCycle);
  • 더 짧고, 명료하고, 성능도 원래 버전과 비등함
  • 안전하지 않은 형변환을 쓰지 않고, 맵의 키인 열거 타입이 그 자체로 출력용 문자열을 제공 (출력 결과에 직접 레이블 달 일 없음)

EnumMap의 성능이 ordinal을 쓴 배열에 비견되는 이유는?

  • 그 내부에서 배열을 사용하기 때문
  • 내부 구현 방식을 안으로 숨겨서 Map의 타입 안전성과 배열의 성능을 모두 얻어냄
  • stream을 활용하면 코드를 더 줄일 수 있음

    • 스트림을 사용하면 EnumMap만 사용했을 때와는 살짝 다르게 동작함

    • EnumMap 버전은 언제나 식물의 생애주기당 하나씩의 중첩 맵을 만들음

    • 스트림 버전에서는 해당 생애주기에 속하는 식물이 있을 때만 만들음

  • 예컨대 정원에 한해살이와 여러해살이 식물만 살고 두해살이는 없다.

  • EnumMap 버전에서는 맵을 3개 만들고 스트림 버 전에서는 2개만 만든다.

두 열거 타입 값들을 매핑하느라 ordinal을 쓴 배열들의 배열

public enum Phase {
	SOLID, LIQUID, GAS;
	
    public enum Transition {
		MELT, FREEZE, BOIL, CONDBJSE, SUBLIME, DEPOSIT;
		
        // 행은 from의 ordinal!, 열은 to의 ordinal올 인덱스로 쓴다.
		private static final Transition[][] TRANSITIONS = {
			{ null, MELT, SUBLIME },
			{ FREEZE, null, BOIL },
			{ DEPOSIT, CONDBJSE, null }
		}// 한 상태에서 다른 상태로의 전이를 반환한다.
		public static Transition from(Phase from, Phase to) {
			return TRANSITIONS[from.ordinaI()] [to.ordinaK)];
		}
	}
}
  • 멋져 보이지만 겉모습에 속으면 안 된다.

  • 컴파일러는 ordinal과 배열 인텍스의 관계를 알 도리가 없다.

    • 즉, Phase나 Phase.Transition 열거 타입을 수정하면서 상전이 표 TRANSITIONS를 함께 수정하지 않거나 실수로 잘못 수정하면 런타임 오류가 날 것이다.
  • 상전이 표의 크기는 상태의 가짓수가 늘어나면 제곱해서 커지며 null로 채워지는 칸도 늘어날 것이다.

결론은 EnumMap

  • 전이 하나를 얻으려면 이전 상태(from)와 이후 상태(to)가 필요하니,맵 2개를 중첩하면 쉽게 해결할 수 있다.
public enum Phase {
	SOLID, LIQUID, GAS;

	public enum Transition {
		MELT(SOLID, LIQUID), FREEZE(LIQUID, SOLID),
		BOIL(LIQUID, GAS), 〔OND며SE(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(g roupingBy(t -> t.from,
			() -> new EnumMap<>(Phase.class),
			toMap(t -> t.to, t -> t,
				(x, y) -> y, 0 -> new EnumMapo(Phase.class))));
	public static Transition from(Phase from, Phase to) {
		return m.get(from).get(to);
		}
	}
}
  • 상전이 맵을 초기화하는 코드는 제법 복잡함

  • 이 맵의 타입인 Map<Phase, Map<Phase, Transition>>은 “이전 상태에서 '이후 상태 에서 전이로의 맵’에 대응시키는 맵”이라는 뜻이다.

요약

  • 배열의 인덱스를 얻기 위해 ordinal을 쓰는 것은 일반적으로 좋지 않으니, 대신 EnumMap올 사용하라.
  • 다차원 관계는 EnumMap<..., EnumMap<...>>으로 표현하라.
  • “애플리케이션 프로그래머는 Enum.ordinal을 (웬만해서는) 사용하지 말아야 한다(아이템 35)”는 일반 원칙의 특수한 사례다

0개의 댓글