열거 타입은 확장할 수 없다.
다시 말해, 열거한 값들을 그대로 가져온 다음 값을 더 추가할 수 없다.
열거 타입이 확장 불가능하도록 설계한 이유는 다음과 같다.
그렇다면 확장할 수 있는 열거 타입은 불가능한 것일까?
다행히도 이 효과를 낼 수 있는 방법이 있다. 바로 인터페이스를 정의하고, 열거 타입이 인터페이스를 구현하도록 하는 것이다.
아이템 34의 Operation 예제를 떠올려보자.
여기에 지수 연산(EXP)와 나머지 연산(REMAINDER)를 추가하는 예제는 다음과 같다.
인터페이스 정의
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;
ExtendedOperation(String symbol) {
this.symbol = symbol;
}
@Override public String toString() {
return symbol;
}
}
새로 추가된 연산은 Operation 인터페이스를 사용하도록 작성되어 있기만 하면 어디든 쓸 수 있다.
또한 apply가 인터페이스에 선언되어 있기 때문에 열거 타입에 따로 추상 메서드로 선언하지 않아도 된다는 점에서 아이템 34의 상수별 메서드 구현과 차이가 있다.
1. 열거 타입의 Class 객체를 이용해 확장된 열거 타입의 모든 원소를 사용하는 예
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));
}
위의 예제에서는 ExtendedOperation의 class 리터럴을 넘겨 확장된 연산들을 모두 출력한다.
여기서 class 리터럴은 한정적 타입 토큰
역할을 한다.
🔖 opEnumType 매개변수
<T extends Enum<T> & Operation> Class<T>
의 의미
= Class 객체가열거 타입
인 동시에Operation의 하위 타입
이어야 한다.
2. 컬렉션 인스턴스를 이용해 확장된 열거 타입의 모든 원소를 사용하는 예
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));
}
여기서는 Class 객체 대신에 한정적 와일드카드 타입
인 Collection<? extends Operation>
을 사용하였다.
이 예제에서는 test 메서드가 좀 더 유연하다. 여러 구현 타입의 연산을 조합해 호출할 수 있기 때문이다.
반면에, 특정 연산에서는 EnumSet과 EnumMap을 사용하지 못한다.
이렇게 인터페이스를 이용해 확장 가능한 열거 타입을 흉내내는 방식에도 한 가지 문제가 있다.
바로 열거 타입끼리 구현을 상속할 수 없다는 점이다.
📌 핵심 정리
열거 타입 자체는 확장할 수 없지만,
인터페이스
와 그인터페이스를 구현하는 기본 열거 타입
을 함께 사용해 같은 효과를 낼 수 있다. 이렇게 하면 클라이언트는 이 인터페이스를 구현해 자신만의 열거 타입(혹은 다른 타입)을 만들 수 있다.
API가 기본 열거 타입을 직접 명시하지 않고 인터페이스 기반으로 작성되었다면 기본 열거 타입의 인스턴스가 쓰이는 모든 곳을 새로 확장한 열거 타입의 인스턴스로 대체해 사용할 수 있다.