이펙티브 자바, 쉽게 정리하기 - item 37.
ordinal
인덱싱 대신EnumMap
을 사용하라
ordinal()
을 배열 인덱스로 이용한 예제
static 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 "Plant{" +
"name='" + name + '\'' +
", lifeCycle=" + lifeCycle +
'}';
}
}
@Test
public void plantsByLifeCycleTest() {
Set<Plant>[] plantsByLifeCycle = (Set<Plant>[]) new Set[Plant.LifeCycle.values().length];
List<Plant> garden = new ArrayList<>(List.of(
new Plant("A", Plant.LifeCycle.ANNUAL),
new Plant("B", Plant.LifeCycle.PERENNIAL),
new Plant("C", Plant.LifeCycle.BIENNIAL),
new Plant("D", Plant.LifeCycle.ANNUAL)
));
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", Plant.LifeCycle.values()[i], plantsByLifeCycle[i]);
}
}
plantsByLifeCycle
을Set
의 배열로values().length
만큼 생성해두었다.- 그리고
ordinal()
을 통해 인덱스를 짚고 있다.
- 그리고
ordinal()
을 통한 방식의 단점
Set
클래스는 제네릭 타입을 받는데, 제네릭 타입은 배열과 호환성이 좋지 않다. 그래서 비검사 형변환을 수행해야 하고, 깔끔히 컴파일되지 않는다.- 정확한 정수값을 사용하는지 스스로 보증해야 한다. 열거 타입만큼 명확하지 않다.
EnumMap
을 사용하여 동일한 기능을 구성한 예
@Test
public void plantEnumMapTest() {
List<Plant> garden = new ArrayList<>(List.of(
new Plant("A", Plant.LifeCycle.ANNUAL),
new Plant("B", Plant.LifeCycle.PERENNIAL),
new Plant("C", Plant.LifeCycle.BIENNIAL),
new Plant("D", Plant.LifeCycle.ANNUAL)
));
Map<Plant.LifeCycle, Set<Plant>> plantsByLifeCycle = new EnumMap<>(Plant.LifeCycle.class);
for (Plant.LifeCycle lifeCycle : Plant.LifeCycle.values()) {
plantsByLifeCycle.put(lifeCycle, new HashSet<>());
}
for (Plant plant : garden) {
plantsByLifeCycle.get(plant.lifeCycle).add(plant);
}
System.out.println("plantsByLifeCycle = " + plantsByLifeCycle);
}
ordinal()
을 사용할 때보다 코드가 많이 깔끔해졌다.- 제네릭 타입도 이전에 배열로 하던 방식과 다르게 컴파일 경고나 오류 없이 이용할 수 있다.
EnumMap
은 성능도 좋다. (내부적으로는 배열을 사용한다.)
람다로 코드 약간 줄여보기
@Test
public void plantEnumMapTestWithLambda() {
List<Plant> garden = new ArrayList<>(List.of(
new Plant("A", Plant.LifeCycle.ANNUAL),
new Plant("B", Plant.LifeCycle.PERENNIAL),
new Plant("C", Plant.LifeCycle.BIENNIAL),
new Plant("D", Plant.LifeCycle.ANNUAL)
));
EnumMap<Plant.LifeCycle, Set<Plant>> lifeCycleSetEnumMap =
garden.stream().collect(
groupingBy(
p -> p.lifeCycle,
() -> new EnumMap<>(Plant.LifeCycle.class),
toSet()
)
);
System.out.println("lifeCycleSetEnumMap = " + lifeCycleSetEnumMap);
}
- 스트림을 이용하여 이전보다 코드를 약간 더 짧게 만들었다.
ordinal()
을 이용하여 2차원 배열의 인덱스로 이용한 예제
enum Phase {
SOLID, LIQUID, GAS;
enum Transition {
MELT, FREEZE, BOIL, CONDENSE, SUBLIME, DEPOSIT;
private static final Transition[][] TRANSITIONS = {
{null, MELT, SUBLIME}, // SOLID
{FREEZE, null, BOIL}, // LIQUID
{DEPOSIT, CONDENSE, null} // GAS
};
public static Transition from(Phase from, Phase to) {
return TRANSITIONS[from.ordinal()][to.ordinal()];
}
}
}
Phase
는 각각 다른 상태로 변화할 수 있는 열거 형태이다.SOLID
toLIQUID
와 같이 전환 형태에 따른enum
타입을 2차원 배열에 지정해두었다.SOLID
toLIQUID
의 경우엔MELT
가 되는 식이다.
- 겉모습은 멋지지만, 컴파일러는
ordinal
과 배열 인덱스의 관계를 모른다.Phase
혹은Transition
을 수정하며 표를 깜빡하고 수정하지 않는 경우가 생길 수 있다.
- 테이블이 커질수록
null
로 채워지는 공간도 넓어질 수 있다.
예제에서 코드를 간단히 하기 위해
null
을 사용했지만, 실제로는 그러지 않는 것이 좋다.
이는
EnumMap
을 2개 중첩하면 쉽게 해결할 수 있다.
EnumMap
을 이용하여 2차원 배열의 인덱스로 이용한 예제
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);
private final Phase from;
private final Phase to;
Transition(Phase from, Phase to) {
this.from = from;
this.to = to;
}
private final static Map<Phase, Map<Phase, Transition>> map =
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 map.get(from).get(to);
}
}
}
- 이전의 코드보다 훨씬 명확해졌다.
PLASMA
라는 새로운PHASE
가 생겼지만, 전혀 기존 코드에 영향을 주지 않는다.(x, y) -> y
부분은 원래 중복된 키에 값이 들어왔을 때 어떻게 합칠까를 관여하는 부분인데, 여기서는 중복된 키가 없으므로 쓰이지 않고 있다.
핵심 정리
- 배열의 인덱스를 위해
ordinal()
을 쓰는 것은 일반적으로 좋지 않다. - 대신
EnumMap
을 사용하자. - 다차원 관계는
EnumMap<..., EnumMap<...>>
으로 표기하자. ordinal()
은 웬만해선 쓰지 말자.
반응형
'Java > 이펙티브 자바' 카테고리의 다른 글
이펙티브 자바, 쉽게 정리하기 - item 39. 명명 패턴보다 애너테이션을 사용하라 (0) | 2022.05.24 |
---|---|
이펙티브 자바, 쉽게 정리하기 - item 38. 확장할 수 있는 열거 타입이 필요하면 인터페이스를 사용하라 (0) | 2022.02.24 |
이펙티브 자바, 쉽게 정리하기 - item 36. 비트 필드 대신 EnumSet을 사용하라 (0) | 2022.02.24 |
이펙티브 자바, 쉽게 정리하기 - item 35. ordinal 메서드 대신 인스턴스 필드를 사용하라 (0) | 2022.02.24 |
이펙티브 자바, 쉽게 정리하기 - item 34. int 상수 대신 열거 타입을 사용하라 (0) | 2022.02.24 |