핵심 내용
런타임 에러를 발생시키는 oridinal 메소드를 사용하지 말고, EnumMap을 사용하라.
다차원일 경우 EnumMap< ..., EnumMap<...>>을 사용하라.
올바르지 않는 방법
: oridinal()을 배열 인덱스로 사용
public class Plant {
enum LifeCycle { ANNUAL, PERNNIAL, BIENNIAL}
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 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]);
}
}
문제점 1 : 배열과 제네릭의 호환성 문제
Set<Plant>[] plantsByLifeCycle = (Set<Plant>[]) new Set[LifeCycle.values().length];
위 부분에서 배열과 제네릭의 호환성 문제가 발생한다.
문제점 2 : 열거 타입의 ordinal 사용
plantsByLifeCycle[plant.lifeCycle.ordinal()].add(plant);
ordinal() 메소드가 반환하는 값은 열거형의 선언 순서에 대한 정보만 제공하며, 이것이 실제 열거형 상수를 나타내진 않는다.
또한, 열거형의 순서를 바꾸거나 새로운 상수를 추가하는 등의 변경이 있을 때, ordinal()이 반환하는 값도 변경될 수 있다. 이러한 변경은 컴파일 타임이 아닌 런타임에 에러를 발생시키므로 디버깅이 어렵다.
다음으로, ordinal() 메소드가 반환하는 정수 값을 배열의 인덱스로 사용하는 것은 정확한 정수값을 사용하는지 개발자가 직접 보증해야 한다. 예를 들어, LifeCycle 열거형에 새로운 상수가 추가되어 ordinal() 메소드가 반환하는 범위가 바뀌었지만 배열의 크기는 그대로인 경우, ArrayIndexOutOfBoundsException이 발생한다. 또는 잘못된 값을 사용하여 잘못된 동작을 하더라도 에러가 발생하지 않을 수 있다.
올바른 방법
: 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);
}
System.out.println(plantsByLifeCycle);
}
Map<LifeCycle, Set<Plant>> plantsByLifeCycle = new EnumMap<>(LifeCycle.class);
EnumMap의 생성자는 열거 타입의 Class 객체를 인자로 받는다. 이 Class 객체는 한정적 타입 토큰이라고 하며, 이를 통해 런타임에 제네릭 타입 정보를 얻는다. 즉, LifeCycle 열거 타입의 상수만 키로 사용할 수 있게 된다.
장점을 정리하면 아래와 같다.
- 비검사 형변환이 필요없다.
- EnumMap은 내부적으로 배열을 사용하므로 배열의 성능을 유지하면서도, 열거 타입의 타입 안정성을 활용할 수 있다.
- ordinal 메소드를 사용하지 않으므로 열거형 상수의 순서가 변경되었을 때 발생할 수 있는 문제를 피할 수 있다. 또한, 열거형 상수 자체를 키로 사용하므로 명확하게 표현할 수 있다.
- EnumMap은 toString 메소드를 재정의하여 결과를 쉽게 출력할 수 있다.
EnumMap을 사용하면 내부에 배열을 사용해서 낭비되는 공간이 없다 ?
EnumMap의 내부 배열은 열거형 상수의 개수만큼의 공간만 필요하다. 즉, HashMap과 같은 다른 맵 구현체에 비해 공간 효율이 좋다.
EnumMap과 스트림을 같이 사용할 경우
예제 1
// Map 사용
public static void streamEx1(List<Plant> garden) {
Map plantsByLifeCycle = garden.stream().collect(Collectors.groupingBy(plant -> plant.lifeCycle));
System.out.println(plantsByLifeCycle);
}
아직 스알못이지만 .. 대략적으로 코드를 해석해보자면
graden 컬렉션에 저장된 Plant 객체들을 스트림으로 변환 후, Collecters의 groupingBy 메소드를 이용해 Plant 객체의 lifeCycle 속성을 기준으로 그룹화한다. 그 그룹화된 결과를 Map에 저장한다. 이때 Map의 키는 lifeCycle 값이고 값은 Plant 객체들의 리스트이다.
결론적으로 EnumMa이 아닌 Map을 사용하기 때문에 EnumMap의 이점이 사라지게 된다.
// 성능 개선을 위해 EnumMap과 Set 사용
public static void streamEx2(List<Plant> garden) {
Map plantsByLifeCycle = garden.stream().collect(Collectors.groupingBy(plant -> plant.lifeCycle,
() -> new EnumMap<>(LifeCycle.class),Collectors.toSet()));
System.out.println(plantsByLifeCycle);
}
위 코드는 Collectors.groupingBy 메소드에서 추가적인 매개변수를 사용해 원하는 맵 구현체를 명시한다.
() -> new EnumMap<>(LifeCycle.class)
위 부분은 그룹화된 결과를 저장할 Map의 구현체를 명시한다. 여기에서는 EnumMap을 사용하여 공간과 성능 이점을 얻을 수 있다.
Collectors.toSet()
위 부분은 그룹화된 결과를 어떤 형태로 저장할지 명시하는 부분이다. 여기에서는 Set을 사용하여 Plant 객체를 중복없이 저장한다.
예제 2
스트림을 사용하면 EnumMap만 사용할 때와 조금 다르게 동작한다.
EnumMap은 식물의 생애주기당 하나씩의 중첩 맵을 만들지만, 스트림은 해당 생애주기에 속하는 식물이 있을 때만 만든다.
// 잘못된 방법 - ordinal을 두 번 사용
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()];
}
}
}
위 예저는 상태(Phase)를 전이(Transition)와 매핑하는 예제이다.
(ex. LIQUID에서 SOLID의 전이는 FREEZE, LIQUID에서 GAS의 전이는 BOIL)
앞서 살펴봤듯, 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 = 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 transitionMap.get(from).get(to);
}
}
}
// PLASMA 상태 추가
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에 PLASMA를 추가하고, IONIZE(GAS, PLASMA)와 DEIONIZE(PLASMA, GAS)만 추가하면 정상동작한다.
'Language > Java' 카테고리의 다른 글
[effective java] 아이템 39. 명명 패턴보다 애너테이션을 사용하라. (0) | 2023.07.02 |
---|---|
[effective java] 아이템 38. 확장할 수 있는 열거 타입이 필요하면 인터페이스를 사용하라. (0) | 2023.07.02 |
[effective java] 아이템 36. 비트 필드 대신 EnumSet을 사용하라. (0) | 2023.05.14 |
[effective java] 아이템 35. ordinal 메서드 대신 인스턴스 필드를 사용하라. (0) | 2023.05.14 |
[effective java] 아이템 34. int 상수 대신 열거 타입을 사용하라. (0) | 2023.05.14 |