Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
316 changes: 316 additions & 0 deletions docs/아이템 34. int 상수 대신 열거 타입을 사용하라.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,316 @@
# 아이템 34. int 상수 대신 열거 타입을 사용하라


```java
// 코드 34-1 정수 열거 패턴 - 상당히 취약
public class IntEnumFruit {
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;
}
```

- 타입 안전을 보장하지 못한다.
- 표현력이 떨어진다.
- 값이 바뀌면 다시 컴파일해야 한다.
- 정수 열거 그룹에 속한 모든 상수를 순회하는 방법, 상수가 몇 개인지 파악하기 어렵다.

```java
// 코드 34-2 가장 단순한 열거 타입
public class EnumFruit {
enum Apple {
FUJI, PIPPIN, GRANNY_SMITH
}
enum Orange {
NAVEL, TEMPLE, BLOOD
}
}
```

- 위 단점들은 **열거 타입(enum type)**을 사용하면 말끔히 해결해준다.
- 열거 타입의 상수 하나당 자신의 인스턴스를 만들어 public static final 필드로 공개한다.
- 열거 타입은 외부에서 접근할 수 있는 생성자를 제공하지 않아 사실상 final이다.
- 열거 타입은 컴파일 타임 타입 안전성을 제공한다.
- 열거 타입에는 임의의 메서드나 필드 추가가 가능하다.
- 인터페이스를 구현할 수 있다.

```java
// 코드 34-3 데이터와 메서드를 갖는 열거 타입 (211쪽)
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
}
}
```

```java
// 어떤 객체의 지구에서의 무게를 입력받아 여덟 행성에서의 무게를 출력한다. (212쪽)
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));
}
}
```

- 열거 타입을 선언한 클래스 혹은 그 패키지에서만 유용한 기능은 private이나 package-private 메서드로 구현한다.
- 널리 쓰이는 열거 타입은 톱레벨 클래스로 만든다.
- java.math.RoundingMode

```java
package java.math;

@SuppressWarnings("deprecation") // Legacy rounding mode constants in BigDecimal
public enum RoundingMode {
UP(BigDecimal.ROUND_UP),
DOWN(BigDecimal.ROUND_DOWN),
CEILING(BigDecimal.ROUND_CEILING),
FLOOR(BigDecimal.ROUND_FLOOR),
HALF_UP(BigDecimal.ROUND_HALF_UP),
HALF_DOWN(BigDecimal.ROUND_HALF_DOWN),
HALF_EVEN(BigDecimal.ROUND_HALF_EVEN),
UNNECESSARY(BigDecimal.ROUND_UNNECESSARY);

final int oldMode;

private RoundingMode(int oldMode) {
this.oldMode = oldMode;
}

public static RoundingMode valueOf(int rm) {
switch(rm) {

case BigDecimal.ROUND_UP:
return UP;

case BigDecimal.ROUND_DOWN:
return DOWN;

case BigDecimal.ROUND_CEILING:
return CEILING;

case BigDecimal.ROUND_FLOOR:
return FLOOR;

case BigDecimal.ROUND_HALF_UP:
return HALF_UP;

case BigDecimal.ROUND_HALF_DOWN:
return HALF_DOWN;

case BigDecimal.ROUND_HALF_EVEN:
return HALF_EVEN;

case BigDecimal.ROUND_UNNECESSARY:
return UNNECESSARY;

default:
throw new IllegalArgumentException("argument out of range");
}
}
}
```

**상수마다 동작이 달라져야 하는 상황 처리 방법**

```java
// 코드 34-10 switch 문을 이용해 원래 열거 타입에 없는 기능을 수행한다. (219쪽)
public class Inverse {
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("Unknown op: " + op);
}
}

public static void main(String[] args) {
double x = Double.parseDouble(args[0]);
double y = Double.parseDouble(args[1]);
for (Operation op : Operation.values()) {
Operation invOp = inverse(op);
System.out.printf("%f %s %f %s %f = %f%n",
x, op, y, invOp, y, invOp.apply(op.apply(x, y), y));
}
}
}
```

- 사칙연산 계산기를 예를 들면, switch 구문으로 활용해 볼 수 있다.
- 그러나 새로운 상수가 추가되면 case 문이 추가돼야 한다는 단점이 있다.
- 위의 문제는 **상수별 메서드 구현**(constant-specific method implementation)으로 해결할 수 있다.

```java
// 코드 34-6 상수별 클래스 몸체(class body)와 데이터를 사용한 열거 타입 (215-216쪽)
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;
}

@Override
public String toString() {
return symbol;
}

public abstract double apply(double x, double y);

// 코드 34-7 열거 타입용 fromString 메서드 구현하기 (216쪽)
private static final Map<String, Operation> stringToEnum =
Stream.of(values()).collect(
toMap(Object::toString, e -> e));

// 지정한 문자열에 해당하는 Operation을 (존재한다면) 반환한다.
public static Optional<Operation> fromString(String symbol) {
return Optional.ofNullable(stringToEnum.get(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));
}
}
```

- Operation 상수가 stringToEnum 맵에 추가되는 시점은 열거 타입 상수 생성 후 정적 필드가 초기화 될 때다.
- java 8이전에는 values()를 순회하면서 빈 해시맵에 {문자열: 열거 타입 상수} 형태로 추가했을 것이다. 그러나, 열거 타입 상수는 생성자에서 자신의 인스턴스를 맵에 추가할 수 없다.(컴파일 에러나 런타임에 NPE 발생할 수 있다.)
- 열거 타입의 정적 필드 중 열거 타입의 생성자에 접근할 수 있는 것은 상수 변수 뿐이다.
- 열거 타입 생성자가 실행되는 시점에는 정적 필드들이 아직 초기화되기 전이라, 자기 자신을 추가하지 못하게 하는 제약이 꼭 필요하다.
- 열거 타입 생성자에서 같은 열거 타입의 다른 상수에 접근할 수 없다.
- 열거 상수가 public static final 로 선언된 것인데 생성자에서 정적 필드에 접근할 수 없는 제약이 적용된 것이다.
- 상수별 메서드 구현에는 열거 타입 상수끼리 코드 공유하기 어렵다.

```java
// 코드 34-9 전략 열거 타입 패턴 (218-219쪽)
enum PayrollDay {
MONDAY(WEEKDAY), TUESDAY(WEEKDAY), WEDNESDAY(WEEKDAY),
THURSDAY(WEEKDAY), FRIDAY(WEEKDAY),
SATURDAY(WEEKEND), SUNDAY(WEEKEND);
// (역자 노트) 원서 1~3쇄와 한국어판 1쇄에는 위의 3줄이 아래처럼 인쇄돼 있습니다.
//
// MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY,
// SATURDAY(PayType.WEEKEND), SUNDAY(PayType.WEEKEND);
//
// 저자가 코드를 간결하게 하기 위해 매개변수 없는 기본 생성자를 추가했기 때문인데,
// 열거 타입에 새로운 값을 추가할 때마다 적절한 전략 열거 타입을 선택하도록 프로그래머에게 강제하겠다는
// 이 패턴의 의도를 잘못 전달할 수 있어서 원서 4쇄부터 코드를 수정할 계획입니다.

private final PayType payType;

PayrollDay(PayType payType) {
this.payType = payType;
}
// PayrollDay() { this(PayType.WEEKDAY); } // (역자 노트) 원서 4쇄부터 삭제

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));
}
}
```

### 열거 타입은 언제 쓰는게 좋을까?

필요한 원소를 컴파일타임에 다 알 수 있는 상수 집합이라면 항상 열거 타입을 사용하자.
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# 아이템 35. ordinal 메서드 대신 인스턴스 필드를 사용하라


```java
// 인스턴스 필드에 정수 데이터를 저장하는 열거 타입 (222쪽)
public enum BadEnsemble {
SOLO, DUET, TRIO, QUARTET, QUINTET,
SEXTET, SEPTET, OCTET, DOUBLE_QUARTET,
NONET, DECTET, TRIPLE_QUARTET;

public int numberOfMusicians() { return ordinal() + 1; }
}
```

- 열거 타입에서 몇 번째 위치인지 반환하는 ordinal 이라는 메서드를 제공한다.
- 상수 선언 순서를 바꾸는 순간 ordinal()를 사용하는 메서드는 오동작하게 될 것이다.

```java
// 인스턴스 필드에 정수 데이터를 저장하는 열거 타입 (222쪽)
public enum Ensemble {
SOLO(1), DUET(2), TRIO(3), QUARTET(4), QUINTET(5),
SEXTET(6), SEPTET(7), OCTET(8), DOUBLE_QUARTET(8),
NONET(9), DECTET(10), TRIPLE_QUARTET(12);

private final int numberOfMusicians;
Ensemble(int size) { this.numberOfMusicians = size; }
public int numberOfMusicians() { return numberOfMusicians; }
}
```

- 열거 타입 상수에 연결된 값은 ordinal 메서드로 얻지 말고 인스턴스 필드에 저장한다.

> Enum API 문서 내용 중, “ordinal()은 EnumSet, EnumMap 같이 연거 타입 기반의 범용 자료구조에 쓸 목적으로 설계됐다.”
>
Loading