Ordinal 메소드를 기억하고 있나요
: 열거 타입 내에서 열거형 인스턴스의 순서를 반환하는 메소드 인 것을 기억 할 것이다. 어떤 상수의 위치를 반환한다고 생각하면 된다.
정원에 심은 식물들을 생애 주기별로 관리하는 예제를 확인해보자.
public 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 name;
}
}
1. 어떤 프로그래머들은 위 생애 주기를 배열로 관리하며 위 배열의 인덱스는 ordinal 값으로 사용 할 것이다.
public static void indexByOrdinal(List<Plant> garden){
//배열은 제네릭과 호환되지 않으므로 비검사 형변환을 수행해야 하고, 깔끔히 컴파일 되지도 않는다.
Set<Plant>[] plantsByLifeCycle = new Set[Plant.LifeCycle.values().length];
for (int i = 0; i < plantsByLifeCycle.length; i++) {
plantsByLifeCycle[i] = new HashSet<>();
}
for (Plant p : garden) {
plantsByLifeCycle[p.lifeCycle.ordinal()].add(p);
}
//배열은 각 인덱스의 의미를 모르니 출력 결과에 직접 레이블을 달아야 한다.
for (int i = 0; i < plantsByLifeCycle.length; i++) {
System.out.printf("%s: %s%n", Plant.LifeCycle.values()[i], plantsByLifeCycle[i]);
}
}
배열 인덱스로 ordinal()을 사용했을 시의 문제점은 다음과 같다.
- 배열은 제네릭과 호환되지 않으므로 비검사 형변환을 수행해야 하고, 깔끔히 컴파일되지 않는다.
- 배열은 각 인덱스의 의미를 모르므로 출력 결과에 직접 레이블을 달아야 한다.
- 정확한 정수값을 사용한다는 것을 프로그래머가 집접 보증해야 한다.
- 정수는 열거 타입과 달리 타입 안전하지 않기 때문이다.
- 잘못된 동작임에도 계속해서 진행하거나 ArrayIndexOutOfBoundsException을 던질 수 있다.
상당히 구리다. 어떻게 하면 이 문제점을 해결할 수 있을까?
답은 EnumMap을 사용하는 것이다.
2. 위 생애 주기를 EnumMap을 사용해 데이터와 열거 타입을 매핑하는 방법
public static void indexByEnumMap(List<Plant> garden) {
Map<Plant.LifeCycle, Set<Plant>> plantByLifeCycle = new EnumMap<>(Plant.LifeCycle.class);
for (Plant.LifeCycle lc : Plant.LifeCycle.values()) {
plantByLifeCycle.put(lc, new HashSet<>());
}
for (Plant p : garden) {
plantByLifeCycle.get(p.lifeCycle).add(p);
}
//자체적으로 출력용 문자열을 제공한다.
System.out.println(plantByLifeCycle);
}
EnumMap을 이용해 데이터와 열거 타입을 매핑하면, 다음과 같은 장점이 존재한다.
- ordinal()을 사용한 것과 비슷한 성능을 내지만 코드는 더 간단명료해진다.
- 형변환을 사용하지 않으므로 타입 안전하다.
- 맵의 키인 열거 타입이 그 자체로 출력용 문자열을 제공해 직접 레이블을 달 일이 없다.
왜 EnumMap을 사용하면 ordinal을 사용한거랑 비슷한 성능을 가지게 될까?
- EnumMap의 내부에서 배열을 사용하기 때문이다.
- 내부 구현 방식을 안으로 숨겨 Map의 타입 안정성과 배열의 성능을 모두 얻은 방법이다.
- EnumMap의 생성자가 받는 키 타입의 Class 객체는 한정적 타입 토큰이다. 런타임 제네릭 타입 정보를 제공한다.
코드를 더 줄이기 위해 Stream을 사용하는 방법도 있다.
3. Stream을 사용해 Map을 관리하는 방법 -1
static private List<Plant> garden;
public static void main(String[] args) {
garden = Arrays.asList(
new Plant("index:0_ANNUAL_1", Plant.LifeCycle.ANNUAL),
new Plant("index:1_ANNUAL_2", Plant.LifeCycle.ANNUAL),
new Plant("index:2_ANNUAL_3", Plant.LifeCycle.ANNUAL),
new Plant("index:3_BIENNIAL_1", Plant.LifeCycle.BIENNIAL),
new Plant("index:4_PERENNIAL_1", Plant.LifeCycle.PERENNIAL)
);
"stream을 사용한다."
System.out.println(item37.garden.stream().collect(groupingBy(p->p.lifeCycle)));
}
하지만 얘는 EnumMap이 아닌 고유한 맵 구현체를 사용한다.
따라서 EnumMap을 써서 얻은 공간과 성능 이점이 사라진다.
3. Stream을 사용해 Map을 관리하는 방법 -2
"EnumMap을 사용해 데이터와 열거 타입을 매핑한 방법"
System.out.println(item37.garden.stream().collect(groupingBy(p->p.lifeCycle,
() -> new EnumMap<>(Plant.LifeCycle.class), toSet())));
//식물의 생애 주기 당 하나씩 중첩 맵을 만든다.
mapFactory에 원하는 맵 구현체를 명시해 호출할 수 있다.
Stream 버전 (3-1)의 경우 식물의 생애 주기에 속하는 식물이 있을 때만 Map을 만든다.
EnumMap 버전 (3-2)의 경우 식물의 생애 주기당 하나씩 Map을 만든다.
언제나 EnumMap은 ordinal()을 사용하는 것 보다 낫다. 이차원 배열(?)의 경우에도 마찬가지이다.
public enum Phase {
SOLID, LIQUID, GAS;
public enum Transition {
MELT, FREEZE, BOIL, CONDENSE, SUBLIME, DEPOSIT;
//행은 from, 열은 to
private static final Transition[][] TRANSITIONS = {
{null, MELT, SUBLIME},
{FREEZE, null, BOIL},
{DEPOSIT, CONDENSE, null}
};
"억지로 억지로~ ordinal을 두 번 사용했다."
public static Transition from(Phase from, Phase to) {
return TRANSITIONS[from.ordinal()][to.ordinal()];
}
}
}
두 열거타입 값들을 매핑하느라 ordinal을 두 번이나 쓴 나쁜 예가 있다.
이를 사용한다면 다음과 같은 단점이 존재한다.
- 컴파일러가 ordinal과 배열 인덱스의 관계를 알 수 없다.
- 열거 타입을 수정하면서 이차원 표를 함께 수정하지 않거나, 실수로 잘못 수정하면 런타임 오류가 발생 할 것이다.
- ArrayIndexOutOfBoundsException 이나 NullPointerException 을 던질 수도 있다.
- 예외가 없는데도 이상한 방향으로 동작할 수 있다.
- 표의 크기는 상태의 가짓수가 늘어나면 제곱해서 커지며, null로 채워지는 칸도 늘어날 것이다.
이 예시도 EnumMap을 사용하면 위와 같은 단점을 예방할 수 있다.
from, to를 표현하기 위해 Map2개를 중첩하여 표현한다.
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 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);
}
}
}
groupingBy에서 전이를 이전 상태(from) 기준으로 묶고, toMap에서는 이후 상태(to)를 대응시키는 EnumMap을 생성한다.
새로운 상태를 추가하더라도 상태 목록과 전이 목록에만 추가하면 된다.
참 쉽죠?
핵심 정리
배열의 인덱스를 얻기 위해 ordinal을 쓰지 말아라. EnumMap을 사용해라.
'JAVA > Effective Java' 카테고리의 다른 글
item39. 명명 패턴보다 애너테이션을 사용하라 (0) | 2022.06.03 |
---|---|
item 38. 확장할 수 있는 열거 타입이 필요하면 인터페이스를 사용하라 (0) | 2022.06.03 |
item 36. 비트 필드 대신 EnumSet을 사용하라 (0) | 2022.05.31 |
item 35. ordinal 메서드 대신 인스턴스 필드를 사용하라 (0) | 2022.05.31 |
item 34. 상수 대신 열거 타입을 사용하라 (0) | 2022.05.31 |
댓글