반응형
이 포스팅에서 작성하는 내용은 EFFECTIVE JAVA(이펙티브자바) 에서 발췌하였습니다.
아이템 37. ordinal 인덱싱 대신 EnumMap을 사용하라
ordinal 메서드
ex 1) Plant 클래스
public class Plant {
enum LifeCycle { ANNUAL, PERNNIAL, BIENNIAL}
final String name;
final LifeCycle lifeCycle;
Plant(String name, LifeCycle lifeCycle) {
this.name = name;
this.lifeCycle = lifeCycle;
}
@Override
public String toString() {
return name;
}
}
- 정원에 심은 식물들을 배열 하나로 관리하고 이들을 생애주기별로 묶어보는 메서드를 만들어보자.
- 생애주기 별로 총 3개의 집합을 만들고 정원을 한 바퀴 돌며 각 식물을 해당 집합에 넣는다.
ex 2) ordinal()을 배열 인덱스로 사용하는 코드 (안좋은 예)
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]);
}
- 동작하지만 문제가 많은 코드이다.
- 배열은 제네릭과 호환되지 않으니 비검사 형변환을 수행해야 하고 깔끔하게 컴파일 되지 않을 것이다.
- 배열은 각 인덱스의 의미를 모르니 출력 결과에 직접 레이블을 달아야한다.
- 가장 큰 문제는 정확한 정숫값을 사용한다는 것을 직접 보증해야한다는 것이다.
- 정수는 열거 타입과 달리 타입 안전하지 않기 때문이다.
- EnumMap을 이용해 해결할 수 있다.
ex 3) EnumMap을 사용해 데이터와 열거타입을 매핑한 코드
Map<LifeCycle, Set<Plant>> plantsByLifeCycle = new EnumMap<>(LifeCycle.class);
for (LifeCycle lifeCycle : LifeCycle.values()) {
plantsByLifeCycle.put(lifeCycle, new HashSet<>());
}
for (Plant p : garden) {
plantsByLifeCycle.get(p.lifeCycle).add(p);
}
System.out.println(plantsByLifeCycle);
- EnumMap은 열거 타입을 키로 사용하도록 설계한 Map 구현체이다.
- 안전하지 않은 형변환은 쓰지 않고, 맵의 키인 열거 타입이 그 자체로 출력용 문자열을 제공하니 출력 결과에 직접 레이블을 달 일도 없다.
- 배열 인덱스를 계산하는 과정도 필요 없어 오류가 발생할 확률이 낮아진다.
- EnumMap의 생성자가 받는 키 타입의 Class 객체는 한정적 타입 토큰으로, 런타임 제네릭 타입 정보를 제공한다.
ex4) 스트림을 사용한 코드 (EnumMap을 사용하지 않은 예)
System.out.println(Arrays.stream(garden).collect(groupingBy(p -> p.lifeCycle)));
- 이 코드는 EnumMap이 아닌 고유한 맵 구현체를 사용했기 때문에 EnumMap을 써서 얻은 공간과 성능 이점이 사라진다는 문제가 있다.
ex 5) 스트림을 사용한 코드 (EnumMap을 사용한 예)
System.out.println(Arrays.stream(garden).collect(groupingBy(p -> p.lifeCycle,
() -> new EnumMap<>(LifeCycle.class), toSet())));
- 스트림을 사용하면 EnumMap만 사용했을 때와는 살짝 다르게 동작한다.
- EnumMap 버전은 언제나 식물의 생애주기당 하나씩의 중첩 맵을 만들지만, 스트림 버전에서는 해당 생애주기에 속하는 식물이 있을 때만 만든다.
ex 6) 배열들의 배열 인덱스에 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()];
}
}
}
- 위의 코드는 작동하지만 좋지 않다.
- Phase나 Phase.Transition 열거 타입을 수정하면 원하는 동작이 일어나지 않는다.
- 또한, TRANSITIONS 배열의 크기는 상태의 가짓수가 늘어나면 제곱해서 커지며 null로 채워지는 칸도 많아질 것이다.
- 즉, 이 경우에도 EnumMap을 사용하는 것이 좋다.
ex 7) 중첩 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;
}
// 전이 Map 을 초기화함
private static final Map<Phase, Map<Phase, Transition>> m = Stream.of(values())
.collect(Collectors.groupingBy(t -> t.from,
() -> new EnumMap<>(Phase.class),
Collectors.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);
}
}
}
- 전이 하나를 얻기 위해 맵 2개를 중첩한 코드 즉, 안쪽 맵은 이전 상태와 전이를 연결하고 바깥 맵은 이후 상태와 안쪽 맵을 연결한 것
- 전이 전후의 두 상태를 전이 열거 타입 Transition의 입력으로 받아, 이 Transition 상수들로 중첩된 EnumMap을 초기화 하면 된다.
ex 8) 새로운 상태인 플라스마가 추가된 코드
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에 상태 추가 및 Phase.Transition에 전이 상태 2개를 추가하면 된다.
정리
- 배열의 인덱스를 얻기 위해 ordinal을 쓰는 것은 일반적으로 좋지 않으니, EnumMap을 사용하자
반응형
'BE > Java' 카테고리의 다른 글
[Effective Java] 아이템 39. 명명 패턴보다 애너테이션을 사용하라 (0) | 2022.10.27 |
---|---|
[Effective Java] 아이템 38. 확장할 수 있는 열거 타입이 필요하면 인터페이스를 사용하라 (0) | 2022.10.26 |
[Effective Java] 아이템 36. 비트 필드 대신 EnumSet을 사용하라 (0) | 2022.10.24 |
[Effective Java] 35. ordinal 메서드 대신 인스턴스 필드를 사용하라 (0) | 2022.10.24 |
[Effective Java] 34. int 상수 대신 열거 타입을 사용하라 (0) | 2022.10.21 |