-
Notifications
You must be signed in to change notification settings - Fork 1
item 34 JihoonKim
hoonti06 edited this page Aug 8, 2020
·
4 revisions
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 // 경고 메시지 하나 뜨지 않는다.
- 포현력도 좋지 않다.
- namespace를 지원하지 않아 접두어를 쓰는 등의 방법으로 이름 충돌을 방지해야 한다
- 컴파일하면 그 값이 클라이언트 파일에 그대로 새겨진다.(JSL-13.1)
- 상수의 값이 바뀌면 클라이언트도 반드시 다시 컴파일해야 함
- 정수 대신 문자열 상수를 사용하는 변형 패턴(문자열 열거 패턴, string enum pattern)도 존재한다
- 완전한 형태의 클래스(C, C++, C#의 enum과는 다르다)
- 상수 하나당 자신의 인스턴스를 하나씩 만들어 public static final 필드로 공개한다
- 밖에서 접근할 수 있는 생성자를 제공하지 않아 사실상 final
- 클라이언트가 인스턴스를 직접 생성하거나 확장할 수 없다
- 열거 타입 선언으로 만들어진 인스턴스들은 딱 하나씩만 존재
- 컴파일타임 타입 안전성 제공
- 매개변수로 건네받은 참조는 null 또는 해당 enum 중 하나(다른 타입일 경우 컴파일 에러)
- namespace 존재하여 이름이 같은 상수도 공존 가능
- 필드의 이름만 공개되기 때문에 enum에 상수 추가나 순서 변경이 있더라도 다시 컴파일하지 않아도 된다.
- enum의 toString()은 출력에 적합한 문자열을 내준다.
- 임의의 메서드나 필드를 추가할 수 있고, 임의의 인터페이스를 구현하게 할 수도 있다.
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),
URANUS(8.683e+25, 2.556e7),
NEPTUNE(1.024e+26, 2.447e7);
// 모든 field final
private final double mass; // 질량(단위: 킬로그램)
private final double radius; // 반지름(단위: 미터)
private final double surfaceGravity; // 표면중력(단위: m / s^2)
// 중력상수 (단위: m^3 / kg s^2)
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 surfaceWeight(double mass) {
return mass * surfaceGravity;
}
}
- 열거 타입 상수 각각을 특정 데이터와 연결지으려면 생성자에서 데이터를 받아 인스턴스 필드에 저장하면 된다.
- 모든 field는 final이어야 한다. (참고로, field를 private으로 두고 public 접근자 메서드를 두는 게 낫다)
- 자신 안에 정의된 상수들의 값을 배열에 담아(인스턴스 배열) 반환하는 정적 메서드 values()를 제공한다. (선언된 순서)
- 상수 이름을 입력받아 그 이름에 해당하는 상수(인스턴스)를 변환해주는 valueOf(String)이 자동 생성된다.
- 각 열거 타입 값의 toString()은 상수 이름을 문자열로 반환한다
- 상수가 제거되면 해당 상수를 참조하고 있던 클라이언트는 컴파일할 때 오류가 발생할 것이다.(정수 열거 패턴에서는 이런 걸 기대할 수 없다)
// 좋지 않은 형태
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);
}
}
- throw문에 도달할 일이 없지만 생략하면 컴파일되지 않는다.
- 상수가 새로 추가되었을 때 case문이 추가되지 않으면 런타임에 오류가 발생한다.
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; } };
private final String symbol;
// 생성자
Operation(String symbol) { this.symbol = symbol; }
public abstract double apply(double x, double y);
@override public String toString() { return symbol; }
}
public static void main(String[] args) {
double x = Double.parseDouble(args[0]);
double y = Double.parseDouble(args[1]);
for (Operation op : Operation.values())
System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y));
}
- apply()가 추상 메서드라서 재정의하지 않으면 컴파일 에러가 발생한다.
- 열거 타입 상수끼리 코드를 공유하기 어렵다는 단점이 있다.
// 좋지 않은 형태
enum PayrollDay {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY;
private static final int MINS_PER_SHIFT = 8 * 60;
int pay(int minutesWorked, int payRate) {
int basePay = minutesWorked * payRate;
int overtimePay;
switch(this) {
case SATURDAY: case SUNDAY: // 주말
overtimePay = basePay / 2;
break;
default: // 주중
overtimePay = minutesWorked <= MINS_PER_SHIFT ? 0 :
(minutesWorked - MINS_PER_SHIFT) * payRate / 2;
}
return basePay + overtimePay;
}
}
- 간결하나, 관리 관점에서 위엄한 코드
- 새로운 값을 enum에 추가하려면 case문을 잊지 말고 쌍으로 넣어줘야 하는 것
- 상수별 메서드 구현은 코드가 장황해져 가독성이 크게 떨어지고 오류 발생 가능성이 높다
// 전략(strategy) 열거 타입 패턴 적용
enum PayrollDay {
MONDAY(WEEKDAY), TUESDAY(WEEKDAY), WEDNESDAY(WEEKDAY),
THURSDAY(WEEKDAY), FRIDAY(WEEKDAY),
SATURDAY(WEEKEND), SUNDAY(WEEKEND);
private final PayType payType;
PayrollDay(PayType payType) {
this.payType = payType;
}
int pay(int minutesWorked, int payRate) {
return payType.pay(minutesWorked, payRate);
}
// nested class
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);
}
}
}
- 잔업 수당 계산을 private 중첩 enum 타입인 PayType에 위임한다.
- switch문보다는 복잡하지만 더 안전하고 유연하다
// Thirdparty에서 가져온 Operation 열거 타입을 이용해야 할 때
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);
}
}
- switch문이 열거 타입의 상수별 동작을 구현하는 데 적합하지 않으나, 상수별 동작을 혼합에 넣을 때는 좋은 선택이 될 수 있다.
- 열거 타입이 정수 상수보다 더 읽기 쉽고 안전하고 강력하다
- 각 상수를 특정 데이터와 연결짓거나 상수마다 다르게 동작하게 할 수 있다.
- 하나의 메서드가 상수별로 다르게 동작해야 할 때는 switch문 대신 상수별 메서드 구현을 사용하자
- 상수 일부가 같은 동작을 공유한다면 전략 열거 타입 패턴을 사용하자
- 필요한 원소를 컴파일타임에 다 알 수 있는 상수 집합이라면 항상 열거 타입을 사용하자
- E.g. 태양계 행성, 체스 말, 메뉴 아이템, 연산 코드, 명령줄 플래그 등
- 정의된 상수 개수가 영원히 불변일 필요는 없다. (상수 추가돼도 바이너리 호환 가능)