아이템 34. int 상수 대신 열거 타입을 사용하라. - ksw6169/effective-java GitHub Wiki
- 열거 타입은 일정 개수의 상수 값을 정의한 다음 그 외의 값은 허용하지 않는 타입이다.
- 사계절, 태양계의 행성, 카드게임의 카드 종류 등이 좋은 예다.
public enum Apple { FUJI, PIPPIN, GRANNY_SMITH }
public enum Orange { NAVEL, TEMPLE, BLOOD }
- 열거 타입은 하나의 클래스로 상수 하나당 자신의 인스턴스를 하나씩 만들어 public static final 필드로 공개한다.
- 클라이언트가 인스턴스를 직접 생성하거나 확장할 수 없으니 열거 타입 선언으로 만들어진 인스턴스들은 딱 하나씩만 존재함을 보장한다.
- 다른 타입의 값을 넘기려 하면 컴파일 오류가 발생한다.
- Apple, Orange와 같은 열거 타입 내에 각각 같은 이름의 상수가 있더라도 충돌하지 않는다.
- 각 열거 타입 값의 toString 메소드는 상수 이름을 문자열로 반환한다.
- 또한 열거 타입은 Object 메소드들과 Comparable, Serializable을 잘 구현해 놓았기 때문에 문제없이 사용 가능하다.
- 열거 타입에는 상수 이름을 입력 받아 그 이름에 해당하는 상수를 반환해주는 valueOf 메소드가 자동 생성된다.
- 열거 타입은 자신 안에 정의된 상수들의 값을 배열에 담아 반환하는 정적 메소드인 values 를 제공한다.
- 값들은 선언된 순서대로 저장된다.
- 열거 타입 상수를 특정 데이터와 연결지으려면 생성자에서 데이터를 받아 인스턴스 필드에 저장하면 된다.
public enum Planet {
MERCURY(3.302e+23, 2.439e6),
VENUS(4.869e+24, 6.052e6),
...
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;
surfaceGravity = G * mass / (radius * radius);
}
...
}
- 열거 타입 상수는 생성자에서 자신의 인스턴스를 맵에 추가할 수 없다. 열거 타입 생성자가 실행되는 시점에는 정적 필드들이 아직 초기화되지 않았기 때문이다. (따라서 자기 자신을 추가하지 못하게 하는 제약이 있다.)
- 열거 타입 생성자에서 같은 열거 타입의 다른 상수에 접근할 수 없다. (열거 타입의 각 상수는 해당 열거 타입의 인스턴스를 public static final로 선언한 것이므로 열거 타입 생성자에서 정적 필드에 접근할 수 없다는 제약이 있다.)
열거 타입을 사용할 때 상수마다 동작이 달라져야 하는 상황이 있을 수 있다. 이 때 switch 문을 사용한다면 다음과 같이 사용할 수 있다. 하지만 열거 타입에 상수가 추가되었을 때 case문을 깜빡하고 추가해주지 않으면 런타임 에러가 발생할 수 있다.
public enum Operation {
PLUS, MINUS, TIMES, DIVIDE;
public double apply(double x, double y) {
switch(this) {
case PLUS: return x + y;
case MINUS: return x - y;
case TIMES: return x * y;
case DIVIDE: return x / y;
}
throw new AssertionError("알 수 없는 연산: " + this);
}
}
열거 타입은 상수별로 다르게 동작하는 코드를 구현하는 더 나은 수단을 제공한다. 즉, 열거 타입에 추상 메소드를 선언하고 각 상수에서 자신에 맞게 재정의하는 방법이다. 이를 상수별 메소드 구현이라 한다.
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; }},
DIVIDE { public double apply(double x, double y) { return x / y; }};
public abstract double apply(double x, double y);
}
- 상수별 메소드 구현에는 열거 타입 상수끼리 코드를 공유하기 어렵다는 단점이 있다.
- 이를 해결하기 위해 모든 상수에 메소드를 중복해서 넣어주거나, 별도의 도우미 메소드로 만들어 각 상수가 자신에게 필요한 메소드를 적절히 호출하는 방식을 생각할 수 있지만 두 방식 모두 코드가 장황해지고 가독성이 크게 떨어지고 오류 발생 가능성이 높아진다.
- 따라서 가장 깔끔한 방법으로 중첩 열거 타입을 사용하는 방식을 생각해볼 수 있다.
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);
}
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);
}
}
}
열거 타입의 toString 메소드를 재정의하려거든 toString이 반환하는 문자열을 해당 열거 타입 상수로 변환해주는 fromString 메소드를 함께 제공하는 것을 고려하라.
public enum 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;
Operation(String symbol) {
this.symbol = symbol;
}
@Override
public String toString() {
return symbol;
}
public abstract double apply(double x, double y);
// key: symbol, value: operation
private static final Map<String, Operation> stringToEnum =
Stream.of(values()).collect(Collectors.toMap(Object::toString, e -> e));
// 지정한 문자열에 해당하는 Operation을 (존재한다면) 반환한다.
public static Optional<Operation> fromString(String symbol) {
return Optional.ofNullable(stringToEnum.get(symbol));
}
}
위 예제에서 fromString은 Optional을 반환하는데 이는 주어진 문자열이 가리키는 연산이 존재하지 않을 수 있음을 클라이언트에 알리고 그 상황을 클라이언트에서 대처하도록 한 것이다.
기존 열거 타입에 상수별 동작을 혼합해 넣을 때는 switch 문이 좋은 선택이 될 수 있다. 즉, 기존 열거 타입에 없는 기능을 수행하고자 할 때(다른 상수에 정의된 동작을 수행하게 한다거나 등등) switch 문을 사용할 수 있다.
public static Operation inverse(Operation op) {
switch(op) {
case PLUS: return Operation.MINUS;
case MINUS: return Operation.PLUS;
case TIMES: return Operation.DIVIDE;
case DIVIDE: return Operation.TIMES;
default: throw new AssertionError("알 수 없는 연산: " + op);
}
}
대부분의 경우 열거 타입의 성능은 정수 상수와 별반 다르지 않다. 열거 타입을 메모리에 올리는 공간과 초기화하는 시간이 들긴 하지만 체감될 정도는 아니다.
필요한 원소를 컴파일 타임에 다 알 수 있는 상수 집합이라면 항상 열거 타입을 사용해야 한다.
- Effective Java 3/E