Set<T>
과 같이 일반적인 제너릭 형태에서는 한 컨테이너가 다룰 수 있는 매개변수의 수가 제한된다.
만약 이보다 유연한 수단이 필요할 때 타입 안전 이종 컨테이너 패턴을 사용할 수 있다.
컨테이너 대신 키를 매개변수화하고, 컨테이너에 값을 넣거나 뺄 때 매개변수화한 키를 함께 제공하는 것을 타입 안전 이종 컨테이너 패턴이라고 한다.
// 각 타입별로 즐겨찾는 인스턴스를 저장하고 검색할 수 있는 클래스
public class Favorites{
public <T> void putFavorite(Class<T> type, T instance);
public <T> T getFavorite(Class<T> type)
}
각 타입의 Class 객체를 매개변수화하여 키 역할로 사용한다.
컴파일 타임 정보와 런타임 타입 정보를 알아내기 위해 메서드들이 주고받는 class 리터럴(Class<T>
)을 타입 토큰이라 부른다.
public static void main(Stringg[] args) {
Favorites f = new Favorites();
f.putFavorite(String.class, "JAVA");
f.putFavorite(Integer.class, 123456);
String favoriteString = f.getFavorite(String.class);
Integer favoriteInteger = f.getFavorite(Integer.class);
System.out.printf("%s", favoriteString); // JAVA 출력
System.out.printf("%d", favoriteInteger); // 123456 출력
}
메서드들이 타입 토큰을 주고받으며 올바른 값을 반환한다.
맵과 달리 여러 가지 타입의 원소를 담을 수 있다.
따라서 Favorites는 타입 안전 이종 컨테이너라 부를 수 있다.
f.putFavorite((Class) Integer.class, "Integer의 인스턴스가 아닙니다."); // 컴파일 에러가 뜨지 않음
int favoriteInteger = f.getFavorite(Integer.class) // ClassCastException 발생
putFavorite를 사용할 때는 정상적으로 동작하지만 getFavorite를 호출하면 ClassCastException이 발생한다.
타입 불변성을 어기는 일이 없도록 보장하려면, putFavorite
메소드에서 동적 형변환 type.cast()
를 해주면 된다.
public <T> void putFavorite(Class<T> type, T instance) {
favorites.put(Objects.requireNonNull(type), type.cast(instance));
}
String
이나 String[]
은 사용할 수 있어도 실체화 불가 타입인 List<String>
은 저장할 수 없다.
List<String>
용 Class 객체를 얻을 수 없기 때문에 코드가 컴파일되지 않는다.List<String>.class
와 List<Integer>.class
는 List.class
라는 Class 객체를 반환한다. -> 대혼란 야기자바에서 열거 타입을 지원하기 전에는 정수 상수를 한 묶음 선언해서 사용하는 정수 열거 패턴을 사용했다.
하지만 정수 열거 패턴에는 많은 단점이 존재한다.
public static final int APPLE_FUJI = 0;
public static final int APPLE_PIPPIN = 1;
public static final int APPLE_GRANNY_SMITH = 2;
public static final int ORANGE_NAVEL = 0;
public static final int ORANGE_TEMPLE = 1;
public static final int ORANGE_BLOOD = 2;
타입 안정성을 보장할 수 없다.
컴파일러 입장에서는 APPLE_FUJI나 ORANGE_NAVEL은 모두 같은 0을 나타내기 때문에 동등 연산자(==)로 비교하더라도 아무런 경고를 발생시키지 않는다.
즉, APPLE_FUJI가 전달되어야 할 값에 ORANGE_NAVEL가 전달되어도 아무런 문제 없이 컴파일된다는 뜻이다.
따라서 타입 안정성을 보장할 수 없다.
정수 열거 패턴을 사용한 프로그램은 깨지기 쉽다.
문자열로 출력하기 까다롭다.
public enum Apple { FUJI, PIPPIN, GRANNY_SMITH }
public enum Orange { NAVEL, TEMPLE, BLOOD }
Java의 열거 타입은 완전한 형태의 클래스이다.
상수 하나당 자신의 인스턴스를 하나씩 만들어 public static final 필드로 공개한다.
인스턴스가 하나씩만 존재함을 보장한다.
생성자를 제공하지 않으므로 사실상 final이다.
따라서 인스턴스를 생성하거나 확장할 수 없으니, 인스턴스가 하나씩만 존재함을 보장할 수 있다.
타입 안정성을 제공한다.
namespace를 제공한다.
public enum Apple { FUJI, PIPPIN, GRANNY_SMITH }
public enum Orange { FUJI, PIPPIN, GRANNY_SMITH }
public enum Planet {
MERCURY(3.302e+23,2.439e6),
VENUS(4.869e+24,6.052e6),
EARTH(5.975e+24, 6.378e6),
MARS(6.419e+23,3.393e6),
JUPITER(1.899e+27,7.149e7),
SATURN(5.685e+26,6.027e7),
URAUS(8.683e+25,2.556e7),
NEPTUNE(1.024e+26,2.477e7);
// 임의의 필드
private final double mass; // 질량
private final double radius; // 반지름
private final double surfaceGravity; // 표면중력
//중력상수
private static final double G = 6.67300E-11;
//생성자
Planet(double mass, double radius) {
this.mass = mass;
this.radius = radius;
this.surfaceGravity = G * mass / (radius * radius);
}
public double mass() {
return mass;
}
public double radius() {
return radius;
}
public double surfaceGravity() {
return surfaceGravity;
}
// 임의의 메서드
public double surfaceWeight(double mass) {
return mass * surfaceGravity;
}
}
상수마다 동작이 달라져야 하는 상황이라면?
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;
}
};
public abstract double apply(double x, double y);
}
apply라는 추상 메서드로 인해 새로운 상수가 추가되면 재정의를 강제한다.
재정의되지 않으면 컴파일 오류로 알려준다.
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);
}
위의 코드에서 toString을 상수의 이름이 아닌 연산기호를 반환하도록 재정의한 것을 볼 수 있다.
상수별 메서드 구현에는 열거 타입 상수끼리 코드를 공유하기 어렵다는 단점이 있다.
이 단점을 보완하려면 전략 열거 타입 패턴을 사용하자.
전략 열거 타입 패턴은 상수를 추가할 때 전략을 선택하도록 하는 패턴이다.
private 중첩 열거 타입을 만들고 계산을 위임한다.
그리고 바깥 열거 타입 생성자에서 전략을 인자로 받게 하면 된다.
// 잔업 수당을 계산해준다.
// 주중에 오버타임, 주말은 무조건 잔업 수당이 발생한다!
public enum PayrollDay {
MONDAY(PayType.WEEKDAY),
TUESDAY(PayType.WEEKDAY),
WEDNESDAY(PayType.WEEKDAY),
THURSDAY(PayType.WEEKDAY),
FRIDAY(PayType.WEEKDAY),
SATURDAY(PayType.WEEKEND),
SUNDAY(PayType.WEEKEND);
private final PayType payType;
PayrollDay(PayType payType) {
this.payType = payType;
}
int pay(int minutesWorked, int payRate) {
return payType.pay(minutesWorked,payRate);
}
// 전략 열거 타입, private 중첩 열거 타입
private enum PayType {
WEEKDAY {
int overtimePay(int minutesWorked, int payRate) {
return minutesWorked <= MINS_PER_SHIFT ?
0 : (minutesWorked - MINS_PER_SHIFT) * payRate / 2;
}
},
WEEKEND {
int overtimePay(int minutesWorked, int payRate) {
return minutesWorked * payRate / 2;
}
};
abstract int overtimePay(int minutesWorked, int payRate);
private static final int MINS_PER_SHIFT = 8 * 60;
int pay(int minutesWorked, int payRate) {
int basePay = minutesWorked * payRate;
return basePay + overtimePay(minutesWorked,payRate);
}
}
}