배열이나 리스트에서 원소를 꺼낼 때 ordinal 메서드 아이템 35로 인덱스를 얻는 코드가 있다.
/**
* 식물을 나타내는 클래스
*/
public 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;
}
}
정원에 심은 식물들을 배열 하나로 관리하고, 이들을 생애주기(한해살이, 여러해살이, 두해살이)별로 묶는다.
생애주기 별로 총 3개의 집합을 만들고 정원을 한 바퀴 돌며 각 식물을 해당 집합에 넣는다.
이때, 집합들을 배열 하나에 넣고, 생애주기의 ordinal 값을 그 배열의 인덱스로 사용할 수 있다.
class Client {
public void addPlant(List<Plant> garden) {
// 생애주기 3가지로 만들어지는 Set
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]);
}
}
}
정원의 식물 입력 테스트
class ClientTest {
private List<Plant> garden;
@BeforeEach
void setUp() {
garden = Arrays.asList(
new Plant("ANNUAL_TREE_1", Plant.LifeCycle.ANNUAL),
new Plant("ANNUAL_TREE_2", Plant.LifeCycle.ANNUAL),
new Plant("ANNUAL_TREE_3", Plant.LifeCycle.ANNUAL),
new Plant("BIENNIAL_TREE_1", Plant.LifeCycle.BIENNIAL),
new Plant("PERENNIAL_TREE_1", Plant.LifeCycle.PERENNIAL)
);
}
// ANNUAL: [ANNUAL_TREE_3, ANNUAL_TREE_2, ANNUAL_TREE_1]
// PERENNIAL: [PERENNIAL_TREE_1]
// BIENNIAL: [BIENNIAL_TREE_1]
@DisplayName("정원에 있는 식물 등록하기")
@Test
void testCase1() {
// given
Client client = new Client();
// then
client.addPlant(garden);
}
}
위 코드에 문제가 한가득이다.
배열은 제네릭과 호환되지 않으니 비검사 형변환을 수행해야 하고, 깔끔하게 컴파일되지 않는다. 아이템 28
배열은 각 인덱스의 의미를 모르니 출력 결과에 직접 레이블을 달아야 한다.
가장 심각한 문제는 인수 값으로 정확한 정숫값을 사용하고 있다는 것을 개발자가 직접 보장해야 한다.
정수는 열거 타입과 달리 타입 안전하지 않기 때문이다.
잘못된 값을 사용하는 경우 배열의 인덱스에 따른 값이 의도한 값을 보장할 수 없고, ArrayIndexOutOfBoundsException을 발생할 수 있다.
해결책
배열은 실질적으로 열거 타입 상수를 값으로 매핑하는 역할을 한다.
이는 Map을 사용할 수 있도록 한다.
열거 타입을 키로 사용하도록 설계 한 아주 빠른 Map의 구현체(EnumMap) 가 존재한다.
EnumMap을 사용 예시
Enum 타입을 Key 값으로 하는 EnumMap 구현체
class Client {
public void addPlantTypeEnumMap(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 을 사용하여 구현된 테스트
class ClientTest {
private List<Plant> garden;
@BeforeEach
void setUp() {
garden = Arrays.asList(
new Plant("ANNUAL_TREE_1", Plant.LifeCycle.ANNUAL),
new Plant("ANNUAL_TREE_2", Plant.LifeCycle.ANNUAL),
new Plant("ANNUAL_TREE_3", Plant.LifeCycle.ANNUAL),
new Plant("BIENNIAL_TREE_1", Plant.LifeCycle.BIENNIAL),
new Plant("PERENNIAL_TREE_1", Plant.LifeCycle.PERENNIAL)
);
}
// {ANNUAL=[ANNUAL_TREE_3, ANNUAL_TREE_2, ANNUAL_TREE_1], PERENNIAL=[PERENNIAL_TREE_1], BIENNIAL=[BIENNIAL_TREE_1]}
@DisplayName("EmumMap을 이용해 정원의 식물 등록하기")
@Test
void testCase2() {
Client client = new Client();
// then
client.addPlantTypeEnumMap(garden);
}
}
Set[] 을 사용하여 관리하는 것보다 EnumMap을 사용하는 것이 간결하고 성능도 비슷하다.
안전하지 않은 형변환을 쓰지 않고, Map의 Key인 열거 타입이 그 자체로 출력용 문자열을 제공하기 때문에 출력 결과에 레이블을 달 일도 없다.
배열 인덱스를 계산하는 과정에서 오류가 날 가능성도 배제할 수 있다.
EnumMap의 성능이 ordinal을 쓴 배열에 비견되는 이유는 그 내부에서 배열을 사용하기 때문이다.
내부 구현 방식을 안으로 숨겨서 Map의 타입 안전성과 배열의 성능을 모두 얻어낸 것이다.
EnumMap의 생성자가 받는 키 타입의 Class 객체는 한정적 타입 토큰으로, 런타임 제네릭 타입 정보를 제공한다. 아이템 33