Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
227 changes: 227 additions & 0 deletions docs/아이템 37. ordinal 인덱싱 대신 EnumMap을 사용하라.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
# 아이템 37. ordinal 인덱싱 대신 EnumMap을 사용하라

```java
// EnumMap을 사용해 열거 타입에 데이터를 연관시키기 (226-228쪽)
// 식물을 아주 단순하게 표현한 클래스 (226쪽)
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;
}
}
```

```java
public static void main(String[] args) {
Plant[] garden = {
new Plant("바질", LifeCycle.ANNUAL),
new Plant("캐러웨이", LifeCycle.BIENNIAL),
new Plant("딜", LifeCycle.ANNUAL),
new Plant("라벤더", LifeCycle.PERENNIAL),
new Plant("파슬리", LifeCycle.BIENNIAL),
new Plant("로즈마리", LifeCycle.PERENNIAL)
};

// 코드 37-1 ordinal()을 배열 인덱스로 사용 - 따라 하지 말 것! (226쪽)
Set<Plant>[] plantsByLifeCycleArr = (Set<Plant>[]) new Set[LifeCycle.values().length];
for (int i = 0; i < plantsByLifeCycleArr.length; i++) {
plantsByLifeCycleArr[i] = new HashSet<>();
}
for (Plant p : garden) {
plantsByLifeCycleArr[**p.lifeCycle.ordinal()**].add(p);
}
// 결과 출력
for (int i = 0; i < plantsByLifeCycleArr.length; i++) {
System.out.printf("%s: %s%n", LifeCycle.values()[i], plantsByLifeCycleArr[i]);
}
}
```

- ordinal() 값을 배열의 인덱스로 사용하는 경우 다음과 같은 **문제점**이 있다.
- 배열은 제네릭과 호환되지 않아(아이템 28) 비검사 형변환을 수행해야한다.
- 배열은 각 인덱스의 의미를 모르니 출력 결과에 직접 레이블을 달아야한다.
- 잘못된 인덱스를 사용하는 경우 ArrayIndexOutOfBoundsException이 발생할 수 있다.

```java
public static void main(String[] args) {
Plant[] garden = {
new Plant("바질", LifeCycle.ANNUAL),
new Plant("캐러웨이", LifeCycle.BIENNIAL),
new Plant("딜", LifeCycle.ANNUAL),
new Plant("라벤더", LifeCycle.PERENNIAL),
new Plant("파슬리", LifeCycle.BIENNIAL),
new Plant("로즈마리", LifeCycle.PERENNIAL)
};
// 코드 37-2 EnumMap을 사용해 데이터와 열거 타입을 매핑한다. (227쪽)
Map<LifeCycle, Set<Plant>> plantsByLifeCycle = new EnumMap<>(LifeCycle.class);
for (LifeCycle lc : LifeCycle.values()) {
plantsByLifeCycle.put(lc, new HashSet<>());
}
for (Plant p : garden) {
plantsByLifeCycle.get(p.lifeCycle).add(p);
}
System.out.println(plantsByLifeCycle);
}
```

- EnumMap은 열거 타입을 키로 사용하도록 설계된 아주 빠른 Map 구현체이다.
- 배열을 사용했을 때보다 다음과 같은 개선점이 있다.
- 코드가 더 짧고 안전하고 성능도 비등하다.
- 안전하지 않은 형변환을 사용하지 않는다.
- 배열 인덱스를 계산하는 과정에서 오류가 발생할 가능성이 없다.
- EnumMap의 성능이 ordinal을 사용한 배열과 비등한 이유는 내부에서 배열을 사용하기 때문이다.

```java
public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V> implements java.io.Serializable, Cloneable {
/**
* The {@code Class} object for the enum type of all the keys of this map.
*
* @serial
*/
private final Class<K> keyType;

/**
* All of the values comprising K. (Cached for performance.)
*/
private transient K[] keyUniverse;

/**
* Array representation of this map. The ith element is the value
* to which universe[i] is currently mapped, or null if it isn't
* mapped to anything, or NULL if it's mapped to null.
*/
private transient Object[] vals;

/**
* The number of mappings in this map.
*/
private transient int size = 0;

/**
* Creates an empty enum map with the specified key type.
*
* @param keyType the class object of the key type for this enum map
* @throws NullPointerException if {@code keyType} is null
*/
public EnumMap(Class<K> keyType) {
this.keyType = keyType;
keyUniverse = getKeyUniverse(keyType);
vals = new Object[keyUniverse.length];
}
...
}
```

- EnumMap의 생성자가 받는 키 타입의 Class 객체는 한정적 타입 토큰으로, 런타임 제네릭 타입 정보를 제공한다(아이템 33)

```java
public static void main(String[] args) {
Plant[] garden = {
new Plant("바질", LifeCycle.ANNUAL),
new Plant("캐러웨이", LifeCycle.BIENNIAL),
new Plant("딜", LifeCycle.ANNUAL),
new Plant("라벤더", LifeCycle.PERENNIAL),
new Plant("파슬리", LifeCycle.BIENNIAL),
new Plant("로즈마리", LifeCycle.PERENNIAL)
};
// 코드 37-3 스트림을 사용한 코드 1 - EnumMap을 사용하지 않는다! (228쪽)
System.out.println(Arrays.stream(garden)
.collect(groupingBy(p -> p.lifeCycle)));

// 코드 37-4 스트림을 사용한 코드 2 - EnumMap을 이용해 데이터와 열거 타입을 매핑했다. (228쪽)
System.out.println(Arrays.stream(garden)
.collect(groupingBy(p -> p.lifeCycle,
() -> new EnumMap<>(LifeCycle.class), toSet())));
}
```

- 코드 37-3의 경우 EnumMap이 아닌 고유한 맵 구현체를 사용했기 때문에 EnumMap의 이점이 사라진다.
- 코드 37-4와 같이 맵 구현체를 지정하는 것이 가능하다.
- EnumMap만 사용했을때와 다르게 스트림을 사용한 경우, 데이터가 존재하는 경우에만 생성하는 차이점이 있다.

```java
public enum BadPhase {
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(BadPhase from, BadPhase to) {
return TRANSITIONS[from.ordinal()][to.ordinal()];
}
}
}
```

- 컴파일러는 ordinal과 배열 인덱스의 관계를 알 수 없다.
- BadPhase.Transition 열거 타입을 수정하면서 TRANSITIONS 필드에 정의된 내용을 수정하지 않으면 런타임 오류가 발생 할 것이다.
- IndexOutOfBoundsException, NPE 등이 발생할 수 있다.

```java
// 코드 37-6 중첩 EnumMap으로 데이터와 열거 타입 쌍을 연결했다. (229-231쪽)
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);

// // 코드 37-7 EnumMap 버전에 새로운 상태 추가하기 (231쪽)
// 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);

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);
}
}

// 간단한 데모 프로그램 - 깔끔하지 못한 표를 출력한다.
public static void main(String[] args) {
for (Phase src : Phase.values()) {
for (Phase dst : Phase.values()) {
Transition transition = Transition.from(src, dst);
if (transition != null)
System.out.printf("%s에서 %s로 : %s %n", src, dst, transition);
}
}
}
}
```

- 배열을 사용하기 보단 EnumMap을 활용하는 것이 좋다.
Loading