열거타입 (enum) : 일정 개수의 상수 값을 정의한 다음 그외의 값은 허용하지 않는 타입
정수 열거 패턴 (int enum pattern) : 이전까지 사용하던 패턴
1. 정수 열거 패턴 (int enum pattern)의 단점
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에서 Orange를 사용해도 컴파일러의 경고 메세지가 없다. - 접두어를 사용한 이름 충돌을 방지하는 방법을 사용한다.
- 평범한 상수 나열이라, 컴파일 하면 그 값이 그대로 새겨지기 때문에 프로그램이 깨지기 쉽다.
- 정수 열거 그룹에 속한 모든 상수를 한 바퀴 순회하는 방법도 마땅치 않으며 상수가 몇개인지도 알 수 없다.
2. 문자열 열거 패턴(string enum pattern)
정수 열거 패턴보다 더 나쁘다
private final String APPLE = "1";
private final String GRAPE = "2";
private final String ORANGE = "3";
- 문자열에 오타가 있어도 컴파일러에서 확인할 길이 없어 런타임 버그
- 문자열 비교에 따른 성능저하
3. 열거 타입 (enum)
public enum Apple {FUJI, PIPPIN, GRANNY_SMITH}
public enum Orange {NAVEL, TEMPLE, BLOOD}
완전한 형태의 클래스 (정수값 뿐인)라서 다른 언어의 열거 타입보다 훨씬 강력하다.
a. 열거 타입의 아이디어
- 열거 타입은 클래스
- 상수 하나당 자신의 인스턴스를 하나씩 만들어서 public static final 필드로 공개한다.
- 열거 타입은 final이다 : 밖에서 접근가능한 생성자를 제공하지 않는다.
- 열거 타입 선언으로 만들어진 인스턴스는 딱 1개만 존재한다.
b. 열거 타입과 싱글턴
- 열거 타입은 인스턴스 통제클래스이다 : 언제 어느 인스턴스를 살아 있게 할지를 통제할 수 있음 (정적 팩터리 방식 클래스)
- 싱글턴 = 원소가 하나뿐인 열거타입
- 열거타입 = 싱글턴을 일반화한 형태
c. 열거타입의 장점
- 컴파일 타입 안전성 제공 : Apple 열거타입 인수에 Orange를 넘길 수 없음
- 이름 같은 상수 공존 : 각자의 이름공간이 있기 때문! 공개 되는 것이 필드의 이름이라 상수 값이 클라이언트에 컴파일 되어 각인되지 않기 때문이다.
- 임의의 메서드나 필드를 추가할 수 있고 임의의 인터페이스를 구현하게 할 수 있다.
- 상수를 하나 제거했을 때 : 제거한 상수를 참조하지 않는 클라이언트에 아무 영향이 없다.
참조를 한 클라이언트에서는 컴파일(런타임-다시 컴파일 X일 때) 오류가 발생할 것! (정수 열거 패턴에서는 기대할 수 없는 대응)
4. 데이터와 메서드를 갖는 열거 타입
각 상수와 연관된 데이터를 해당 상수 자체에 내재시킨다.
고차원의 추상 개념 하나를 표현 할 수 있다.
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.477e7);
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;
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; // F = ma
}
}
열거 타입 상수 각각을 특정 데이터와 연결지을 때 생성자에서 데이터를 받아 인스턴스 필드에 저장한다.
- 열거 타입은 근본적으로 불변이라 모든 필드는
final
이야 한다. - 필드를
private
으로 두고 별도의public
접근자 메서드를 두자
a. 열거타입의 배열 values()
자신 안에 정의된 상수들의 값을 배열에 담아 반환하는 정적 메서드 값들은 선언된 순서로 저장된다.
public class WeightTable {
public static void main(String[] args) {
double earthWeight = Double.parseDouble(args[0]);
double mass = earthWeight / Planet.EARTH.surfaceGravity();
for (Planet p : Planet.values())
System.out.printf("%s에서의 무게는 %f이다.%n",
p, p.surfaceWeight(mass));
}
}
b. 열거타입을 올바르게 사용하기
- 일반 클래스와 마찬가지로 기능을 클라이언트에게 노출해야할 합당한 이유가 없다면
private
으로, 혹은 (필요하다면)package-private
으로 선언하라 - 널리 쓰이는 열거타입 = 톱레벨 클래스로 구현
- 특정 톱레벨 클래스에서만 사용 = 해당 클래스의 멤버 클래스로 구현
5. 상수별 메서드 구현(constant-specific method implementation)
switch를 이용한 구현은 새로운 상수를 추가할 때마다 해당 case문도 추가해야해서 깨지기 쉽다.
상수별 메서드 구현
열거 타입에 추상 메서드를 선언하고, 각 상수별 클래스 몸체(constant-specific class body)를 각 상수에 맞게 재정의하는 방법
import java.util.*;
import java.util.stream.Stream;
import static java.util.stream.Collectors.toMap;
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);
}
열거 타입의 valueOf(string)
상수 이름을 입력받아 이 이름에 해당하는 상수를 반환해 주는 메서드
fromString 메서드 제공
열거타입의 toString을 재정의 할 때 함께 제공하는 걸 고려해보자.
toString이 반환하는 문자열을 해당 열거 타입 상수로 변환해주는 메서드
@Override public String toString() { return symbol; }
// 지정한 문자열에 해당하는 Operation을 (존재한다면) 반환한다.
public static Optional<Operation> fromString(String symbol) {
return Optional.ofNullable(stringToEnum.get(symbol));
}
열거타입 정적 필드의 생성 시점
private static final Map<String, Operation> stringToEnum =
Stream.of(values()).collect(
toMap(Object::toString, e -> e));
Operation 상수가 stringToEnum 맵에 추가되는 시점 : 열거타입 생성 후 정적 필드가 초기화 될 때
열거 타입 상수는 생성자에서 자신의 인스턴스를 맵에 추가할 수 없다 : 컴파일 오류
- 열거 타입의 정적 필드 중 열거 타입 생성자에서 접근 할 수 잇는 것은 상수 변수 뿐이다.
- 열거 타입 생성자 실행 시점에는 정적 필드 초기화 전이다.
- 열거 타입 생성자에서 같은 열거 타입의 다른 상수에도 접근 할 수 없다.
(열거 타입의 인스턴스를 public static final으로 선언함. 다른 형제 상수도 static이므로 열거 타입 생성자에서 정적 필드에 접근할 수 없다는 제약이 적용된다.)
6. 상수별 동작 혼합 : 전략 열거 타입 패턴
열거 타입 상수 일부가 같은 동작을 공유한다면 전략 열거 타입 패턴을 사용하자.
package effectivejava.chapter6.item34;
import static effectivejava.chapter6.item34.PayrollDay.PayType.*;
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);
}
enum PayType {
WEEKDAY {
int overtimePay(int minsWorked, int payRate) {
return minsWorked <= MINS_PER_SHIFT ? 0 :
(minsWorked - MINS_PER_SHIFT) * payRate / 2;
}
},
WEEKEND {
int overtimePay(int minsWorked, int payRate) {
return minsWorked * payRate / 2;
}
};
abstract int overtimePay(int mins, int payRate);
private static final int MINS_PER_SHIFT = 8 * 60;
int pay(int minsWorked, int payRate) {
int basePay = minsWorked * payRate;
return basePay + overtimePay(minsWorked, payRate);
}
}
public static void main(String[] args) {
for (PayrollDay day : values())
System.out.printf("%-10s%d%n", day, day.pay(8 * 60, 1));
}
}
추가하려는 메서드가 의미상 열거타입에 속하는 경우 다음과 같이 전략 열거 타입 패턴을 사용한다.
그렇지 않은 경우에는 switch를 적용해서 간단하게 만든다.
7. 열거타입을 사용해야 할 때
-
필요한 원소를 컴파일 타임에 다 알 수 있는 상수 집합이라면 항상 열거 타입을 사용하자
Ex) 태양계 행성, 한 주의 요일, 체스말
-
열거 타입에 정의된 상수 개수가 영원히 고정 불변일 필요는 없다.
Ex) 메뉴 아이템, 연산 코드, 명령줄 플래그
-
열거타입의 성능은 상수와 별반 다르지 않다
'Dev Book Review > Effective Java' 카테고리의 다른 글
[Effective Java] item 36. 비트 필드 대신 EnumSet을 사용하라 (0) | 2020.06.27 |
---|---|
[Effective Java] item 35. ordinal 메서드 대신 인스턴스 필드를 사용하라 (0) | 2020.06.27 |
[Effective Java] Chapter 5: 제네릭 (0) | 2020.05.19 |
[Effective Java] item33. 타입 안전 이종 컨테이너를 고려하라 (0) | 2020.05.19 |
[Effective Java] item32. 제네릭과 가변인수를 함께 쓸 때는 신중하라 (0) | 2020.05.19 |