enum 타입이 등장하기 전에는 정수 열거 패턴(int enum pattern)을 사용했었다.
public static final int APPLE_FUJI = 0;
public static final int APPLE_PIPPIN = 1;
public static final int ORANGE_NEVEL = 0;
public static final int ORANGE_TEMPLE = 1;
열거 타입은 확실히 정수 상수보다 뛰어나다.
해당 상수가 열거 타입에서 몇 번째인지 반환하는 ordinal 메서드를 제공한다.
예를 들어 가장 첫 번째 상수는 0을 반환한다.
열거 타입 상수와 연결된 정숫값이 필요한 경우 ordinal 메서드를 이용하고 싶은 유혹에 빠질 수 있는데, 위험한 선택일 수 있다.
아래 코드를 통해 확인해보자.
합주단의 종류를 연주자가 1명인 솔로(solo)부터 10명인 디텍트(detect)까지 정의한 enum이다.
public enum Ensemble {
SOLO, DUET, TRIO, QUARTET, QUINTET,
SEXTET, SEPTET, OCTET, NONET, DECTET;
public int numberOfMusicians() { return ordinal() + 1; }
}
public enum Ensemble {
SOLO(1), DUET(2), TRIO(3), QUARTET(4), QUINTET(5),
SEXTET(6), SEPTET(7), OCTET(8), NONET(9), DECTET(10),
DOUBLE_QUARTET(8), TRIPLE_QUARTET(12);
private final int int numberOfMusicians;
Ensemble(int size) { this.numberOfMusicians = size; }
public int numberOfMusicians() { return numberOfMusicians; }
}
열거 타입 상수에 연결된 값은 인스턴스 필드에 저장하자.
과거에는 아래와 같이 정수 열거 패턴을 비트 필드 표현에 사용했다.
public class Text {
public static final int STYLE_BOLD = 1 << 0; // 1
public static final int STYLE_ITALIC = 1 << 1; // 2
public static final int STYLE_UNDERLINE = 1 << 2; // 4
public static final int STYLE_STRIKETHROUGH = 1 << 3; // 8
// 매개변수 styles는 0개 이상의 STYLE_ 상수를 비트별 OR한 값이다.
public void applyStyles(int styles) { ... }
}
다음과 같이 비트별 OR를 이용하여 여러 상수를 하나의 집합으로 모을 수 있었는데 이를 비트 필드(bit field)라고 한다.
// Text.STYLE_BOLD | Text.STYLE_ITALIC ===> 결과값은 3
text.applyStyles(STYLE_BOLD | STYLE_ITALIC);
비트별 연산을 이용해 합집합과 교집합 같은 집합 연산을 효율적으로 할 수 있으나, 정수 열거 상수의 단점을 그대로 가지고 있으며 오히려 해석하기가 더 어렵다.
또한 비트 필드에 포함된 모든 의미상의 원소를 순회하기도 까다롭고 최대 몇 비트가 필요한지 미리 예측한 후 타입을 선택해야 한다.
비트를 늘릴 수 없기 때문이다.
이러한 불편함은 EnumSet을 사용하는 것으로 해결할 수 있다.
public class Text {
public enum Style { BOLD, ITALIC, INDERLINE, STRIKETHROUGH }
// 깔끔하고 안전하다. 어떤 Set을 넘겨도 되나, EnumSet이 가장 좋다.
// 보통 인터페이스를 전달 받는 것이 좋은 습관이다.
public void applyStyles(Set<Style> styles) { ... }
}
- EnumSet의 유일한 단점은 불변 EnumSet을 만들 수 없다는 것이다.
- 비트 필드를 사용할 이유는 없다. EnumSet을 사용하자.
ordinal 메서드를 배열 인덱스로 사용하면 위험하다.
아래와 같이 ordinal 메서드를 배열 인덱스로 사용하면 위험하다.
// 배열은 제네릭과 호환되지 않으니 비검사 형변환도 필요
Set<Plant>[] plantByLifeCycle =
(Set<Plant>[]) 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]);
}
EnumMap을 사용하면 안전하다.
위와 같은 문제는 EnumMap을 사용하면 해결된다.
열거 타입을 키로 사용하도록 설계된 아주 빠른 Map 구현체이다.
// EnumMap을 사용하여 데이터와 열거 타입을 매핑한다.
Map<Plant.LifeCycle, Set<Plant>> plantsByLifeCycle =
new EnumMap<>(Plant.LifeCycle.class);
for (Plant.LifeCycle lc : Plant.LifeCycle.values()) {
plantsByLifeCycle.put(lc, new HashSet<>());
}
for (Plant p : garden) {
plantsByLifeCycle.get(p.lifeCycle).add(p);
}
System.out.println(plantsByLifeCycle);
스트림(Stream)을 이용한 방법
스트림을 사용해 맵을 관리하면 코드를 더 줄일 수 있다.
// Map을 이용해 데이터와 열거 타입 매핑
Arrays.stream(garden)
.collect(groupingBy(p -> p.lifeCycle))
// EnumMap을 이용해 데이터와 열거 타입 매핑
Arrays.stream(garden)
.collect(groupingBy(
p -> p.lifeCycle,
() -> new EnumMap<>(LifeCycle.class),
toSet())
);
배열의 인덱스를 얻을 때는 EnumMap을 사용하자.
열거 타입을 확장하는 것은 대부분 좋지 않다.
하지만 연산 코드(operation code)를 구현할 때는 어울릴 수 있다.
이때는 열거 타입 enum이 인터페이스를 구현(implements)할 수 있다는 점을 이용하면 된다.
public interface Operation {
double apply(double x, double y);
}
public enum BasicOperation implements Operation {
PLUS("+") {
public double apply(double x, double y) { return x + y; }
},
MINUS("-") {
public double apply(double x, double y) { return x - y; }
},
TIMES("*") {
public double apply(double x, double y) { return x * y; }
},
DIVIDE("/") {
public double apply(double x, double y) { return x / y; }
};
private final String symbol;
BasicOperation(String symbol) { this.symbol = symbol; }
@Override public String toString() { return symbol; }
}
public enum ExtendedOperation implements Operation {
EXP("^") {
public double apply(double x, double y) {
return Math.pow(x, y);
}
},
REMAINDER("%") {
public double apply(double x, double y) {
return x % y;
}
};
private final String symbol;
// 생성자, toString 생략
}
타입 수준에서도 기본 열거 타입 대신에 확장한 열거 타입을 넘겨서 열거 타입의 모든 원소를 순회하게 할 수 있다.
public static void main(String[] args) {
double x = Double.parseDouble(args[0]);
double y = Double.parseDouble(args[1]);
test(ExtendedOperation.class, x, y);
}
private static <T extends Enum<T> & Operation> void test(Class<T> opEnumType, double x, double y) {
for (Operation op : opEnumType.getEnumConstants()) {
System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y));
}
}
위와 다르게 한정적 와일드카드 타입을 사용하는 방법도 있다.
열거 타입의 리스트를 전달하여 한정적 와일드 카드 타입으로 지정한다.
public static void main(String[] args) {
double x = Double.parseDouble(args[0]);
double y = Double.parseDouble(args[1]);
test(Arrays.asList(ExtendedOperation.values()), x, y);
}
private static void test(Collection<? extends Operation> opSet, double x, double y) {
for (Operation op : opSet) {
System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y));
}
}
한편 열거 타입끼리 구현을 상속할 수 없는 문제는 있다.
열거 타입 간의 공유하는 기능이 늘어나 코드 중복량이 많아진다면 Helper 클래스 또는 메서드로 분리하면 좋다.
열거 타입은 인터페이스 구현을 통해 확장 효과를 낼 수 있다.