전공공부
[아이템 38] 확장 할 수 있는 열거 타입이 필요하면 인터페이스를 사용하라 본문
결론
열거 타입 자체는 확장 할 수 없다.
하지만 인터페이스를 이용하여 인터페이스를 구현하는 기본 열거 타입을 함께 사용해 확장과 같은 효과를 낼 수 있다.
타입 안전 열거 패턴(typesafe enum pattern)
public class TypesafeOperation {
private final String type;
private TypesafeOperation(String type) {
this.type = type;
}
public String toString() {
return type;
}
public static final TypesafeOperation PLUS = new TypesafeOperation("+");
public static final TypesafeOperation MINUS = new TypesafeOperation("-");
public static final TypesafeOperation TIMES = new TypesafeOperation("*");
public static final TypesafeOperation DIVIDE = new TypesafeOperation("/");
}
타입 안전 열거 패턴은 모두 열거 타입 보다 열등하지만 클래스를 이용하기 때문에 열거 타입과 달리 확장을 할 수 있다.
(열거 타입은 Enum<T>를 내부적으로 상속 받고 있기 때문에 추가 상속이 불가능하다.)
물론, 대부분의 상황에서 열거 타입을 확장하려는 시도가 좋은 생각은 아니다. 확장 타입 원소는 기반 타입 원소로 취급되어지지만 그 반대의 경우는 성립하지 않고 기반 타입 및 확장 타입을 모두 순회 할 수 있는 방법이 존재하지 않는다.
그럼에도 불구하고 확장이 필요한 열거 타입이 필요한 경우가 있다. 연산 코드인데, 이따금 API가 제공하는 연산 코드 외에도 사용자 확장 연산이 필요 할 수 있다.
인터페이스를 활용한 확장
인터페이스는 다중 상속이 가능하고 extends와 겹치지 않아서 열거 타입에서도 사용이 가능하다.
그러니 인터페이스를 사용하여서 확장성을 늘릴 수 있다.
Operation 인터페이스
@FunctionalInterface
public interface Operation {
double apply(double x, double y);
}
BaseOperation.java
public enum BasicOperation implements Operation {
PLUS("+") {
@Override
public double apply(double x, double y) { return x + y; }
},
MINUS("-") {
@Override
public double apply(double x, double y) { return x - y; }
},
TIMES("*") {
@Override
public double apply(double x, double y) { return x * y; }
},
DIVIDE("/") {
@Override
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; }
}
열거 타입 각각의 상수에서 Operation 인터페이스의 apply 메서드를 구현한다.
이 외의 연산이 필요하다면 우리는 Operation 인터페이스를 구현한 열거 타입을 작성 하는 것 뿐이다.
ExtenedOperation.java
public enum ExtendedOperation implements Operation {
EXP("^") {
@Override
public double apply(double x, double y) { return Math.pow(x, y); }
},
REMAINDER("%") {
@Override
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; }
}
ExtendedOperation은 BaseOperation을 사용 할 수 있는 위치에서 어디든지 가져와서 사용 할 수 있다.
이렇게 구현한 인터페이스와 열거 타입은 열거 타입 내부에서 따로 추상 메서드를 선언하여 구현하지 않아도 되고 인터페이스 덕분에 다형성도 보장이 된다.
인터페이스를 사용하지 않게 될때의 불편함.
public enum 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;
}
},
DIVDE("/") {
public double apply(double x, double y) {
return x / y;
}
};
private final String symbol;
Operation(String symbol) {
this.symbol = symbol;
}
@Override
public String toString() {
return symbol;
}
public abstract double apply(double x, double y);
}
한정적 타입 토큰을 사용한 예시
public class OperationApp {
public static void main(String[] args) {
double x = 4, y = 2;
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));
}
}
}
ANS
4.000000 ^ 2.000000 = 16.000000
4.000000 % 2.000000 = 0.000000
추가 . getEnumConstans()는 Enum 클래스의 상수를 배열 형태로 넘겨준다.
public T[] getEnumConstants()
test 메서드에 인수로 ExtendedOperation 의 class 리터럴을 넘겨 이를 이용한 방법이다.
쉽게 말해 클래스의 상수들을 가져오기 위해서 class 리터럴을 넘겨서 내부적으로 상수 값들을 조회한다.
opEnumType 매개변수 선언인 이것은 <T extends Enum<T> & Operation> 객체가 열거타입인 동시에 Operation 인터페이스의 구현체여야 한다는 의미다.
한정적 와일드 카드 사용
public static void main(String[] args) {
double x = 4, y = 2;
testV2(Arrays.asList(ExtendedOperation.values()), x, y);
}
private static void testV2(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));
}
}
ANS
4.000000 ^ 2.000000 = 16.000000
4.000000 % 2.000000 = 0.000000
장점
훨씬 간결하다.
test 메서드가 유연해졌다.
단점
EnumSet, EnumMap은 이런 식으로 못 넘겨준다는 단점이 존재한다.
그래도 존재하는 문제점
열거 타입끼리 구현한 객체를 직접 상속 할 수 없다.
그래도, 구현 객체를 쓰는 것 처럼 쓰기 위해서는 인터페이스 자체에 디폴트 구현을 걸어서 사용 할 수 있다.
디폴트 구현의 예시
public interface Operation {
double apply(double x, double y);
default double plus(double x, double y){
return x+y;
}
default double minus(double x, double y){
return x-y;
}
}
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;
}
},
PLUS("+"){
@Override
public double plus(double x, double y) {
return super.plus(x, y);
}
@Override
public double apply(double x, double y) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'apply'");
}
},
MINUS("-") {
@Override
public double minus(double x, double y) {
return super.minus(x, y);
}
@Override
public double apply(double x, double y) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'apply'");
}
};
private final String symbol;
ExtendedOperation(String symbol) {
this.symbol = symbol;
}
@Override public String toString() {
return symbol;
}
}
'Study > Java' 카테고리의 다른 글
[아이템 47] 반환 타입으로는 스트림보다 컬렉션이 낫다. (0) | 2023.05.06 |
---|---|
[아이템 42] 익명 클래스 보다는 람다식을 사용하라 (0) | 2023.05.01 |
[아이템 31] 한정적 와일드카드를 사용해 API 유연성을 높이라 (0) | 2023.04.04 |
아이템 19. 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라 (0) | 2023.03.05 |
아이템 13. clone 재정의는 주의해서 진행해라 (2) | 2023.02.20 |