자바에는 enum과 annotation이라는 것이 있다. 이번 챕터에서는 이 두가지를 잘 사용하는 방법에 대해 살펴보자.
Item 34 : int 상수 대신 enum을 사용하자
기본적인 상수만 있는 enum 말고 동작이 필요한 Enum을 만든다면?
가장 기본적인 구현방법은 아래와 같을 것이다.
// Enum type that switches on its own value - questionable
public enum Operation {
PLUS, MINUS, TIMES, DIVIDE;
// Do the arithmetic operation represented by this constant
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("Unknown op: " + this);
}
}
하지만 해당 코드는 enum에 타입이 추가되었을 때 switch 문에 해당되는 액션을 추가하지 않아도 컴파일 에러가 나지 않는다! 상당히 위험한 코드다.
Constant-specific method implementation ( 상수별 메서드 구현 )
추상 메서드로 이렇게 선언해놓으면 강제화 할 수 있다.
// Enum type with constant-specific method implementations
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);
}
// Implementing a fromString method on an enum type
private static final Map<String, Operation> stringToEnum =
Stream.of(values()).collect(
toMap(Object::toString, e -> e));
// Returns Operation for string, if any
public static Optional<Operation> fromString(String symbol) {
return Optional.ofNullable(stringToEnum.get(symbol));
}
급여 계산 enum
평일과 주말간의 급여를 계산해주는 코드를 작성해보자. 기본적인 구현은 switch를 통해서 아래와 같이 계산을 할 수 있을 것이다.
// Enum that switches on its value to share code - questionable
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: // Weekend
overtimePay = basePay / 2;
break;
default: // Weekday
overtimePay = minutesWorked <= MINS_PER_SHIFT ? 0 : (minutesWorked - MINS_PER_SHIFT) * payRate / 2;
}
return basePay + overtimePay;
}
}
- 장점 : 코드가 간결하고 읽기 좋다
- 단점 : 유지보수에 좋지 않다
- Vacataion day가 추가된다면? 개발자가 실수로 switch 문에 추가를 하지 않아도 컴파일 에러가 나지 않기 때문에 Vacation day 급여도 평일 급여로 계산이 될 것이다.
- 결국 가독성은 점점떨어지고 실수할 확률은 점점 늘어날 것이다.
constant-specific method 대신 abstract method 사용하기
우리가 위에서 생겼던 문제는 enum constant ( 날짜 종류 ) 가 추가되어도 overtime pay 계산하는 함수를 강제하지 못했기 때문에 발생하였다. 계산과 관련된 부분은 PayType inner enum에게 책임을 준다면 된다.
// The strategy enum pattern
enum PayrollDay {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY(PayType.WEEKEND),
SUNDAY(PayType.WEEKEND);
private final PayType payType;
PayrollDay(PayType payType) { this.payType = payType; }
PayrollDay() { this(PayType.WEEKDAY); } // Default
int pay(int minutesWorked, int payRate) {
return payType.pay(minutesWorked, payRate);
}
// The strategy enum type
private 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;
}
}
}
}
enum에 있는 constant-specific한 행동을 할 때는 switch를 사용하는게 더 좋다.
// Switch on an enum to simulate a missing method
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);
}
}
위의 코드도 필요할 수도 있지만 enum에 담을만한 가치가 있지는 않다.
Enum 언제 사용하는게 좋은데?
- 개발자가 compile 시점에 constants에 대해 알아야 할 때
Item35 : ordinal 함수 대신 instance field를 사용하라
// Abuse of ordinal to derive an associated value - DON'T DO THIS
public enum Ensemble {
SOLO, DUET, TRIO, QUARTET, QUINTET,
SEXTET, SEPTET, OCTET, NONET, DECTET;
public int numberOfMusicians() { return ordinal() + 1; }
}
- 문제점 1 : 12명인 enum을 추가하려면 의미 없는 11명 enum도 생성해야 함
- 문제점 2 : 만약에 순서를 실수로 바꾼다면??? 컴파일 에러 없이 프로그램이 다 깨짐
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; }
}
Item 36 : 비트 필드 대신 EnumSet을 이용하라
// Bit field enumeration constants - OBSOLETE!
public class Text {
public static final int STYLE_BOLD = 1 << 0; // 1
public static final int STYLE_ITALIC = 1 << 1; // 2
public static final int STYLE_UNDERLINE = 1 << 2; // 4
public static final int STYLE_STRIKETHROUGH = 1 << 3; // 8
// Parameter is bitwise OR of zero or more STYLE_ constants
public void applyStyles(int styles) { ... }
}
비트를 이용하여 처리하면 합집합으로 사용하기 좋다
text.applyStyles(STYLE_BOLD | STYLE_ITALIC);
- 해석하기 어려움
- 비트 수도 미리 예측 해야함
// EnumSet - a modern replacement for bit fields
public class Text {
public enum Style { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH }
// Any Set could be passed in, but EnumSet is clearly best
public void applyStyles(Set<Style> styles) { ... }
}
text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));
EnumSet으로 받아도 되지만 일반적으로 interface로 받는게 좋은 습관이다. 그리고 EnumSet의 내부는 비트 벡터로 구현되어 있다. 원소가 총 64개 이하라면, long 변수 하나로 표현이 가능하며 비트 필드에 비견되는 성능을 보여준다.
Item 37 : ordinal 인덱싱 대신 EnumMap을 사용하라
구현 : 정원에 심을 식물과 생애주기(한해살이, 두해살이... ) 를 묶어서 출력을 해보자.
class Plant {
enum LifeCycle { ANNUAL, PERENNIAL, BIENNIAL }
final String name;
final LifeCycle lifeCycle;
Plant(String name, LifeCycle lifeCycle) {
this.name = name;
this.lifeCycle = lifeCycle;
}
@Override public String toString() {
return name;
}
}
- 방법 1 : 생애주기별로 for문을 돌면서 식물을 해당 집합에 넣어서 구현 & ordinal() 사용
// Using ordinal() to index into an array - DON'T DO THIS!
Set<Plant>[] plantsByLifeCycle =
(Set<Plant>[]) new Set[Plant.LifeCycle.values().length];
for (int i = 0; i < plantsByLifeCycle.length; i++)
plantsByLifeCycle[i] = new HashSet<>();
for (Plant p : garden)
plantsByLifeCycle[p.lifeCycle.ordinal()].add(p);
// Print the results
for (int i = 0; i < plantsByLifeCycle.length; i++) {
System.out.printf("%s: %s%n",
Plant.LifeCycle.values()[i], plantsByLifeCycle[i]);
}
- 문제점 1 : 배열은 generic과 호환되지 않으므로 비검사 형변환을 수행해야 함
- 문제점 2 : i 가 정확한 정수인지 모른다!
- 방법 2 : EnumMap을 이용
생애주기는 이미 정해진 상수 값이므로 Map을 사용할 수 있다. 그리고 열거 타입을 key로 사용하도록 설계한 아주 빠른 EnumMap이 있다!
// Using an EnumMap to associate data with an enum
Map<Plant.LifeCycle, Set<Plant>> plantsByLifeCycle =
new EnumMap<>(Plant.LifeCycle.class);
for (Plant.LifeCycle lc : Plant.LifeCycle.values())
plantsByLifeCycle.put(lc, new HashSet<>());
for (Plant p : garden)
plantsByLifeCycle.get(p.lifeCycle).add(p);
System.out.println(plantsByLifeCycle);
- 좋은점 1 : 짧고, 명료하고, 안전함
- 좋은점 2 : ordinal() 과 비교해도 떨어지지 않는 성능
-> EnumMap 내부에서도 결국 vals 라는 배열을 이용하기 때문에 성능이 비견 된다.
-> 결국 타입 안정성 & 성능을 모두 얻어냄
public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V>
implements java.io.Serializable, Cloneable
{
// ...
public boolean containsKey(Object key) {
return isValidKey(key) && vals[((Enum<?>)key).ordinal()] != null;
}
// ...
}
- 좋은점 3 : EnumMap의 생성자가 받는 키 타입의 Class 객체는 한정적 타입 토큰으로, 런타임 제네릭 타입 정보를 제공한다. ( Item 33 참고 )
- 방법 3 : Stream을 사용 ( Item 45 )
// Naive stream-based approach - unlikely to produce an EnumMap!
System.out.println(Arrays.stream(garden)
.collect(groupingBy(p -> p.lifeCycle)));
- 좋은점 1 : 코드가 짧아짐
- 단점 1 : EnumMap이 아닌 고유 맵 구현체 사용 -> 공간 성능 이점 사라짐
- 방법 4 : Stream + mapFactory 사용
매개변수 3개짜리 groupingBy에서는 원하는 맵 구현체를 명시해 호출할 수 있다.
// Using a stream and an EnumMap to associate data with an enum
System.out.println(Arrays.stream(garden)
.collect(groupingBy(p -> p.lifeCycle,
() -> new EnumMap<>(LifeCycle.class), toSet())));
구현 상황 : 두 열거 타입 값들을 매핑해야 하는 경우 ( 상태 / 전이 )
예를 들어, 액체(LIQUID)에서 고체(SOLID)로의 전이는 응고(FREEZE)가 된다.
// Using ordinal() to index array of arrays - DON'T DO THIS!
public enum Phase {
SOLID, LIQUID, GAS;
public enum Transition {
MELT, FREEZE, BOIL, CONDENSE, SUBLIME, DEPOSIT;
// Rows indexed by from-ordinal, cols by to-ordinal
private static final Transition[][] TRANSITIONS = {
{ null, MELT, SUBLIME },
{ FREEZE, null, BOIL },
{ DEPOSIT, CONDENSE, null }
};
// Returns the phase transition from one phase to another
public static Transition from(Phase from, Phase to) {
return TRANSITIONS[from.ordinal()][to.ordinal()];
}
}
}
- 좋은점 1 : 그냥 멋저보임
- 단점 1 : 컴파일러는 oridinal과 배열 인덱스의 관계를 알 도리가 없음
- 단점 2 : 상태가 추가되면 null 값과 2차원 배열이 계속 늘어남
- 개선 된 방법 : EnumMap을 사용
// Using a nested EnumMap to associate data with enum pairs
public enum Phase {
SOLID, LIQUID, GAS;
public enum Transition {
MELT(SOLID, LIQUID), FREEZE(LIQUID, SOLID),
BOIL(LIQUID, GAS), CONDENSE(GAS, LIQUID),
SUBLIME(SOLID, GAS), DEPOSIT(GAS, SOLID);
private final Phase from;
private final Phase to;
Transition(Phase from, Phase to) {
this.from = from;
this.to = to;
}
// Initialize the phase transition map
private static final Map<Phase, Map<Phase, Transition>>
m = Stream.of(values()).collect(groupingBy(t -> t.from,
() -> new EnumMap<>(Phase.class),
toMap(t -> t.to, t -> t,
(x, y) -> y, () -> new EnumMap<>(Phase.class))));
public static Transition from(Phase from, Phase to) {
return m.get(from).get(to);
}
}
}
Item 38 : 확장할 수 있는 열거 타입이 필요하면 인터페이스를 사용하라
// Emulated extensible enum using an interface
public interface Operation {
double apply(double x, double y);
}
public enum BasicOperation implements 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; }
};
ITEM 38: EMULATE EXTENSIBLE ENUMS WITH INTERFACES 177
private final String symbol;
BasicOperation(String symbol) {
this.symbol = symbol;
}
@Override
public String toString() {
return symbol;
}
}
enum 타입은 상속이 불가능 하지만, 그래도 Operation interface를 통해 API의 동작을 나타낼 수 있고, 다른 enum타입을 정의 할 수 있다!
// Emulated extension enum
public enum ExtendedOperation implements Operation {
EXP("^") {
public double apply(double x, double y) {
return Math.pow(x, y);
}
},
REMAINDER("%") {
public double apply(double x, double y) {
return x % y;
}
};
private final String symbol;
ExtendedOperation(String symbol) {
this.symbol = symbol;
}
@Override public String toString() {
return symbol;
}
}
public static void main(String[] args) {
double x = Double.parseDouble(args[0]);
double y = Double.parseDouble(args[1]);
test(ExtendedOperation.class, x, y);
}
private static <T extends Enum<T> & Operation> void test(
Class<T> opEnumType, double x, double y) {
for (Operation op : opEnumType.getEnumConstants())
System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y));
}
public static void main(String[] args) {
double x = Double.parseDouble(args[0]);
double y = Double.parseDouble(args[1]);
test(Arrays.asList(ExtendedOperation.values()), x, y);
}
private static void test(Collection<? extends Operation> opSet,
double x, double y) {
for (Operation op : opSet)
System.out.printf("%f %s %f = %f%n",
x, op, y, op.apply(x, y));
}
Item 39 : 명명 패턴보다 annotation을 사용하라
명명패턴의 단점
- JUnit3때처럼 함수 이름 앞에 test가 붙어야 동작하던 시절 : 오타나면? 확인할 방법 없음
- 별도로 보장을 해주지 않음
'Java' 카테고리의 다른 글
Java 8 - stream 예제 모음 (0) | 2019.11.13 |
---|---|
ThreadLocal이란 (0) | 2019.10.29 |
10. Exceptions ( 예외 처리 ) - Effective Java 3th (0) | 2019.08.12 |
Jaxb 하면서 있었던 이슈 (0) | 2019.06.04 |
디자인패턴8-전략 패턴/스트래티지 패턴(Strategy Pattern) (0) | 2019.05.30 |