이따금 배열이나 리스트에서 원소를 꺼낼 때 ordinal 메서드로 인덱스를 얻는 코드가 있다.
이런 코드는 동작은 하지만 문제가 한가득이다.
배열은 제네릭과 호환되지 않으니 비검사 형변환을 수행해야 하고 깔끔히 컴파일되지 않는다.
배열은 각 인덱스의 의미를 모르니 출력 결과에 직접 레이블을 달아야 한다.
가장 심각한 문제는 정확한 정수값을 사용한다는 것을 개발자가 직접 보증해야 한다는 점이다.
정수는 열거 타입과 달리 타입 안전하지 않기 때문에 잘못된 동작이 발생할 수 있다.
public class Plant {
final String name;
final LifeCycle lifeCycle;
public Plant(String name, LifeCycle lifeCycle) {
this.name = name;
this.lifeCycle = lifeCycle;
}
@Override
public String toString() {
return name;
}
}
public enum LifeCycle {
ANNUAL, PERNNIAL, BIENNIAL
}
public static void usingOrdinalArray(List<Plant> garden) {
Set<Plant>[] plantsByLifeCycle = (Set<Plant>[]) new Set[LifeCycle.values().length];
for (int i = 0 ; i < plantsByLifeCycle.length ; i++) {
plantsByLifeCycle[i] = new HashSet<>();
}
for (Plant plant : garden) {
plantsByLifeCycle[plant.lifeCycle.ordinal()].add(plant);
}
for (int i = 0 ; i < plantsByLifeCycle.length ; i++) {
System.out.printf("%s : %s%n",
LifeCycle.values()[i], plantsByLifeCycle[i]);
}
}
ordinal을 인덱스로 사용하는 코드이다.
여기서 배열은 실질적으로 열거 타입 상수를 값으로 매핑하는 일을 한다.
그러니 Map을 사용할 수도 있다.
열거 타입을 키로 사용하도록 설계한 아주 빠른 Map 구현체가 존재하는데, 그것이 EnumMap이다.
public static void usingEnumMap(List<Plant> garden) {
Map<LifeCycle, Set<Plant>> plantsByLifeCycle = new EnumMap<>(LifeCycle.class);
for (LifeCycle lifeCycle : LifeCycle.values()) {
plantsByLifeCycle.put(lifeCycle,new HashSet<>());
}
for (Plant plant : garden) {
plantsByLifeCycle.get(plant.lifeCycle).add(plant);
}
//EnumMap은 toString을 재정의하였다.
System.out.println(plantsByLifeCycle);
}
더 짧고 명료하고 안전하고 성능도 원래 버전과 비등하다.
안전하지 않은 캐스팅은 사용하지 않고, 맵의 키인 열거 타입 그 자체로 출력용 문자열을 제공하니 출력 결과에 레이블을 달 일도 없다.
배열 인덱스를 계산하는 과정에서 오류가 날 가능성도 원천봉쇄할 수 있다.
EnumMap의 성능이 oridinal을 쓴 배열에 비견되는 이유는 내부에서 배열을 사용하기 때문이다.
내부 구현 방식을 안으로 숨겨서 Map의 타입 안전성과 배열의 성능을 모두 얻어낸 것이다.
여기서 EnumMap의 생성자가 받는 키 타입의 Class객체는 한정적 타입 토큰으로, 런타임 제네릭 타입 정보를 제공하는데 아이템 33에서 설명한 것이다.
public static void streamV1(List<Plant> garden) {
Map plantsByLifeCycle = garden.stream().collect(Collectors.groupingBy(plant -> plant.lifeCycle));
System.out.println(plantsByLifeCycle);
}
public static void streamV2(List<Plant> garden) {
Map plantsByLifeCycle = garden.stream()
.collect(Collectors.groupingBy(plant -> plant.lifeCycle,
() -> new EnumMap<>(LifeCycle.class),Collectors.toSet()));
System.out.println(plantsByLifeCycle);
}
스트림을 사용 할 수도 있다.
EnumMap을 사용하는 부분은 맵은 3개를 만들지만 스트림 버전에서는 맵을 2개만 만든다.
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을 사용한 배열이다.
앞서 설명한 문제들처럼, Phase나 Phase.Transition 열거 타입을 수정하면서 상전이 표 TRANSITIONS를 함께 수정하지 않거나 실수로
잘못 수정하면 런타임 에러가 발생할 것이다.
또, 표의 크기는 상태의 가짓수가 늘어나면 제곱해서 커지며 null로 채워야 하는 칸도 늘어난다.
이렇듯 ordinal 배열 인덱싱 대신 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;
}
private static final Map<Phase, Map<Phase, Transition>> transitionMap;
static {
transitionMap = Stream.of(values())
.collect(Collectors.groupingBy(t -> t.from, // 바깥 Map의 Key
() -> new EnumMap<>(Phase.class), // 바깥 Map의 구현체
Collectors.toMap(t -> t.to, // 바깥 Map의 Value(Map으로), 안쪽 Map의 Key
t -> t, // 안쪽 Map의 Value
(x,y) -> y, // 만약 Key값이 같은게 있으면 기존것을 사용할지 새로운 것을 사용할지
() -> new EnumMap<>(Phase.class)))); // 안쪽 Map의 구현체
}
public static Transition from(Phase from, Phase to) {
return transitionMap.get(from).get(to);
}
}
}
이렇게 EnumMap을 사용하여 구현하면 새로운 상태를 추가할 때도 위의 표처럼 제곱으로 일거리가 늘어나지 않고 단순히 필드만 추가해주면 된다.
실제 내부에서도 맵이 배열로 구현되느 낭비되는 공간과 시간도 거의 없이 명확하고 안전하고 유지보수하기에 좋다.
배열의 인덱스를 얻기 위해 ordinal을 쓰는 것은 일반적으로 좋지 않으니 EnumMap을 사용하자.
이 외에도 해당 상황뿐 아니라,
프로젝트를 하며 Enum을 key로 사용할 수 있는 상황이 여러번 있었다.
이럴 때엔 Enum의 value나 name을 가지고 HashMap등을 사용하지 말고, 성능이 보장되는 EnumMap을 사용해보자.
질문 1. ordinal 인덱싱이란 무엇이고, 왜 EnumMap을 사용해야 할까?
ordinal 인덱싱이란 Enum 상수의 순서를 정수 값으로 인덱싱하여 사용하는 방식이다.
열거 타입은 기본적으로 상수에 0부터 시작하는 순서 값을 부여하고, 이를 통해 배열이나 리스트와 같은 인덱스 기반의 자료구조를 활용할 수 있다.
하지만 문제점이 있는데 상수를 추가하거나 제외하면 코드의 오작동이 발생할 수 있고,
인덱스를 직접 사용하기 때문에 가독성이 저하되고 실수로 잘못된 인덱스를 사용할 가능성이 있다.
또한 상황에 따라 인덱스를 맞추기 위한 더미 상수가 들어갈 수도 있고 불필요한 작업이 생길 수 있다.
EnumMap은 이러한 문제점을 해결하기 위해 사용되는데 열거타입을 기반으로 한 효율적인 맵 자료구조이고 특정 열거 타입 값을 가진 맵 엔트리로 표현된다.
EnumMap을 사용하면 열거 상수의 순서나 의미가 변경되어도 영향을 받지 않고 안전하게 사용할 수 있다.
또한 가독성이 좋고 Enum의 특징 중 하나인 컴파일 타입에 타입 체크를 수행하여 타입 안정성을 보장한다.
질문 2. EnumMap을 사용할 때 주의할 점은?
EnumMap은 키와 값에 Null을 허용하지 않는다. 따라서 null을 저장하려하면 NullPointException이 발생한다.
null값을 다뤄야할 경우, Optional또는 특정 값을 사용하여 처리해야 한다.
EnumMap은 특정 열거 타입에 대해서만 사용할 수 있기 때문에 상황에 따라 유연성이 저하될 수 있고,
누락된 상수가 있다면 런타임 에러가 발생할 수 있다.
'JAVA > 이펙티브 자바' 카테고리의 다른 글
39. 명명 패턴보다 애너테이션을 사용하라 (0) | 2023.07.20 |
---|---|
38. 확장할 수 있는 열거 타입이 필요하면 인터페이스를 사용하라 (0) | 2023.07.20 |
35. ordinal 메서드 대신 인스턴스 필드를 사용하라 (0) | 2023.07.18 |
34. int 상수 대신 열거 타입을 사용하라 (0) | 2023.07.18 |
33. 타입 안정 이종 컨테이너를 고려하라 (0) | 2023.07.16 |