ITEM 34. int 상수 대신 열거 타입을 사용하라
** 열거 타입은 일정 개수의 상수 값을 정의한 다음, 그 외의 값은 허용하지 않는 타입니다.
- 정수 열거 패턴(int enum pattern)에 단점
- 타입 안전을 보장하지 않으며, 표현력도 떨어진다.
- 프로그램이 깨지기 쉽다.
- 클라이언트 파일에 그대로 새겨지기 때문에, 상수의 값이 바뀌면 클라이언트도 반드시 다시 컴파일 해야 한다.
- 정수 상수는 문자열로 출력하기 까다롭다.
- 문자열 열거 패턴 (string enum pattern)에 단점
- 오타 등으로 런타임 버그가 발생한다.
- 열거 타입(enum type)의 장점
public enum Apple { FUJI, PIPPIN, GRANNY_SMITH } public enum Orange { NAVEL, TEMPLE, BLOOD }
- 열거 타입 자체는 클래스이며, 상수 하나당 자신의 인스턴스를 만들어 public static final 필드로 공개한다.
- 싱글턴은 원소가 하나뿐인 열거 타입이라고 할 수 있고, 열거 타입은 싱글턴을 일반화한 형태라고 할 수 있다.
- 컴파일 타임 타입 안전성을 제공한다.
- 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(4.685e+26, 2.556e7), 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 } }
- 상수별 메서드 구현
- 상수에서 자신에 맞게 재정의하는 것을 말한다.
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; } }, DIVIDE("/") { public double apply(double x, double y){ return x / y; } }, TIMES("*") { public double apply(double x, double y){ return x * y; } }; public abstract double apply(double x, double y); private final String symbol; Operation(String symbol) { this.symbol = symbol; } @Override public String toString() { return symbol; } }
- 열거 타입에는 상수 이름을 입력받아 그 이름에 해당하는 상수를 반환해주는 valueOf(String) 메서드가 자동 생성된다.
- toString을 재정의할 때에는 toString이 반환하는 문자열을 해당 열거 타입 상수로 변환해주는 fronString 메서드도 함께 제공하자.
private static final Map<String, Operation> stringtoEnum = Stream.of(values()).collect(toMap(Object::toString, e->e)); public static Optional<Operation> fronString(String symbol) { return Optional.ofNullable(stringToEnum.get(symbol)); }
- 상수에서 자신에 맞게 재정의하는 것을 말한다.
- 전략 열거 타입 패턴
public enum PayrollDay { MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY; private static final int MINS_PER_SHIFT = 8 * 60; // 하루 8시간 int pay(int minutesWorked, int payRate) { int basePay = minutesWorked * payRate; int overtimePay; switch(this) { case SATURDAY: case SUNDAY: // 주말 overtimePay = basePay / 2; break; default: // 주중 if (minutesWorked <= MINS_PER_SHIFT) { overtimePay = 0 ; } else { overtimePay = (minutesWorked - MINS_PER_SHIFT) * payRate / 2; } } return basePay + overtimePay; } }
- 상수별 메서드에 열거 타입 상수끼리 코드를 공유하기가 어렵다라는 단점이 있다.
- 새로운 상수를 추가하려면 그 값을 처리하는 case 문을 추가하여야 한다. 또한 잔업 수당 등에 대한 새로운 형태를 제공하기에는 장황해진다.
- 이 때 잔업수당 계산을 private 중첩 열거 타입으로 옯기고 PayrollDay 열거 타입의 생성자에서 이를 선택하게 하면 좀 더 안전하고 유연하게 접근할 수 있다. ```java
public enum PayrollDay {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, 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 minsWorked, int payRate) {
int overtimePay;
if (minsWorked <= MINS_PER_SHIFT) {
overtimePay = 0;
} else {
overtimePay = (minsWorked - MINS_PER_SHIFT) * payRate / 2;
}
return overtimePay;
}
},
WEEKEND {
int overtimePay(int minsWorked, int payRate) {
return minsWorked * payRate / 2;
}
};
abstract int overtimePay(int mins, int payRate);
static final int MINS_PER_SHIFT = 8 * 60; // 하루 8시간
int pay(int minutesWorked, int payRate) {
int basePay = minutesWorked * payRate;
return basePay + overtimePay(minutesWorked, payRate);
}
}
}
- 열거 타입은 필요한 원소를 컴파일타임에 다 알수있는 상수 집합이라면 사용하자.
- 열거 타입에 정의된 사우 개수가 영원히 고정 불변일 필요는 없다.
ITEM 35. ordinal 메서드 대신 인스턴스 필드를 사용하라
- 열거 타입 상수는 하나의 정숫값에 대응하여 ordinal이란 메서드를 통해 몇 번째 위치인지를 반환한다.
public enum Ensemble { SOLO, DUET, TRIO, QUARTET, QUINTET, SEXTET, SEPTET, OCTET, NONET, DECTET; public int numOfMusicians() { return ordinal() + 1; } }
- ordinal 메서드는 유지보수가 어렵다.
- index로 사용하다 보니 의미를 맞추기 위해 빈 값들이 추가되거나 해당 의미에 값을 2개 이상 추가할 수 없다.
- 그렇기 때문에 ordinal 메서드를 사용하지 말고 인스턴스 필드에 저장하자.
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); private final int numberOfMusicians; Ensemble(int numberOfMusicians) { this.numberOfMusicians = numberOfMusicians; } public int numberOfMusicians() { return this.numberOfMusicians; } }
ITEM 36. 비트 필드 대신 EnumSet을 사용하라
- 비트 필드
- 이전에는 열거한 값들이 집합으로 사용되는 경우 2의 거듭제곱 값(비트 필드)으로 할당한 정수 열거 패턴을 사용해왔다.
- 비트 빌드를 사용하면 비트별 연산을 사용해 합집합, 교집합과 같은 집합 연산에 용이하다.
- 다만, 열거 상수의 단점을 그대로 지니며, 가독성이 떨어지는 추가 단점이 있고, 비트를 사용하므로 기본타입 변경이 필요할 수 있다.
- EnumSet
- Set 인터페이스를 완벽히 구현하며 내부는 비트 연산으로 되어있다.
public class Text { public enum Style { BOLD, ITALIC, UNDERLINE; } // EnumSet을 사용하는 것이 좋으며 EnumSet 인스턴스를 건네는 클라이언트 코드다. public void applyStyles(Set<Style> styles) { ... } }
- Set 인터페이스를 완벽히 구현하며 내부는 비트 연산으로 되어있다.
ITEM 37. ordinal 인덱싱 대신 EnumMap을 사용하라
- ordinal 메서드를 배열 인덱스로 사용하면 위험하다.
- ordinal 메서드를 사용하면 정확한 정수값을 사용한다는 것을 개발자가 보증해야 한다.
- EnumMap
- 열거 타입을 키로 사용하도록 설계한 Map 구현체
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); }
- 안전하지 않은 형변환을 사용하지 않으며, index 접근이 아닌 열거 타입을 key로 사용하기 때문에 안전하다.
- 열거 타입을 키로 사용하도록 설계한 Map 구현체
- Stream을 활용한 방법
Arrays.stream(garden).collect(groupingBy(p -> p.lifeCycle)); // 첫번째 코드 Arrays.stream(garden).collect(groupingBy(p-> p.lifeCycle, () -> new EnumMap<>(LifeCycle.class), toSet())); // 두번째 코드
ITEM 38. 확장할 수 있는 열거 타입이 필요하면 인터페이스를 사용하라
- 열거 타입을 확장하기 위해서는 인터페이스를 사용하면 된다.
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; }
},
DIVIDE("/") {
public double apply(double x, double y){ return x / y; }
},
TIMES("*") {
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 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;
Extended(String symbol) { this.symbol = symbol; }
@Override
public String toString() { return symbol; }
}
- BasicOperation은 열거 타입이기 때문에 확장이 불가능하지만 Operation을 통해 확장이 가능하다.
public static void main(String[] args) {
double x = args[0]; double y = args[1];
test1(ExtendedOperation.class, x, y);
test2(Arrays.asList(ExtendedOperation.values()), x, y));
}
private static <T extends Enum<T> & Operation> void test1(Class<T> opEnumType, double x, double y) {
for(Operation op : opEnumType.getEnumConstants()) {
System.out.println("%f %s %f = %f", x, op, y, op.apply(x,y));
}
}
private static void test2(Collection<? extends Operation> opSet, double x, double y) {
for(Operation op : opSet) {
System.out.println("%f %s %f = %f", x, op, y, op.apply(x, y));
}
}
- test1은 <T extends Enum
& Operation>을 통해 열거 타입이며 Operation 구현체를 입력받는 것을 명시한다. - test2는 와일드 카드 타입 Collection<? extends Operation>을 넘기는 방법이다.
ITEM 39. 명명 패턴보다 애너테이션을 사용하라
- 명명 패턴 단점
- jUnit3는 메소드 앞에 test를 붙여 테스트 메서드임을 알려주는 명명패턴을 사용했다.
- 단점1. 만약 오타가 발생하면 jUnit은 메서드를 무시한체 지나갔기 때문에 테스트가 실행됐는 지 확인할 수 없다.
- 단점2. 올바른 프로그램 요소에서만 사용되리라 보증할 방법이 없다.
- 단점3. 매개변수를 전달할 마땅한 방법이 없다.
- Annotation을 사용하자
- 매개변수를 받지 않는 @Test Annotation
@Retetion(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Test {}
- Meta Annotation : Annotation 선언에 다는 Annotation
- @Retention(RetentionPolicy.RUNTIME) : @Test가 런타임에도 유지되어야 한다는 의미
- @Target(ElementType.METHOD) : @Test 애너테이션은 메소드 선언에만 사용되어야 한다는 의미
- 사용 예
public class SampleTest { @Test public static void m1() {} // 성공 public static void m2() {} // 실행하지 않는다. @Test public static void m3() { throw new RuntimeException("실패"); } // 실패 public static void m4() {} // 실행하지 않는다. @Test public void m5() {} // 잘못된 사용 - 정적 메서드가 아니다. public static void m6() {} // 실행하지 않는다. @Test public static void m7() { throw new RuntimeException("실패"); } // 실패 public satic void m8() {} // 실행하지 않는다. } pulbic class SampleMain() { public static void main(String[] args) { int tests = 0; int passed = 0; Class<?> testClass = Class.forName(args[0]); for(Method m : testClass.getDeclaredMethods()) { if(m.isAnnotationPresent(Test.class)) { tests++; try { m.invoke(null); passed++; } catch(InvocationTargetException wrappedException) { Throwable exc = wrappedException.getCause(); System.out.println(m + "실패: " + exc); } catch(Exception e) { System.out.println("잘못 사용한 @Test: " + m); } } } System.out.println("성공: %d 실패: %d", passed, tests-passed); } }
- m2, 4, 6, 8은 실행되지 않고, m1 는 성공, m3, 7은 실패로 떨어진다. m5 는 정적 메서드가 아니기 때문에 잘못 사용한 예가 된다.
- Meta Annotation : Annotation 선언에 다는 Annotation
- 매개변수를 받는 Annotation
@Retetion(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface ExceptionTest { Class<? extends Throwable> value(); }
- 매개변수 타입을 <? extends Throwable> 와일드 카드 타입으로 선언했다.
- Throwable을 확장한 클래스의 Class 객체라는 의미를 담는다.
public class SampleTest { @ExceptionTest(ArithmeticException.class) public static void m1() { int i = 0; i /= i; // 예외 발생 } // 성공 @ExceptionTest(ArithmeticException.class) public static void m2() { int[] a = new int[0]; int i = a[1]; // OutOfIndex 발생 } // 실패, ArithmeticException 이 아니다. @ExceptionTest(ArithmeticException.class) public static void m3() {} // 실패 } pulbic class SampleMain() { public static void main(String[] args) { int tests = 0; int passed = 0; Class<?> testClass = Class.forName(args[0]); for(Method m : testClass.getDeclaredMethods()) { if(m.isAnnotationPresent(Test.class)) { tests++; try { m.invoke(null); System.out.println(m + "실패: 아무런 예외가 발생하지 않는다."); } catch(InvocationTargetException wrappedException) { Throwable exc = wrappedException.getCause(); Class<? extends Throwable> excType = m.getAnnotation(ExceptionTest.class).value(); if(excType.isInstance(exc) { passed++; } else { System.out.println(m + "실패. 기대한 예외: " + excTYpe.getName() + " 발생한 예외: " + exc); } } catch(Exception e) { System.out.println("잘못 사용한 @Test: " + m); } } } System.out.println("성공: %d 실패: %d", passed, tests-passed); } }
- Throwable을 확장한 클래스의 Class 객체라는 의미를 담는다.
- 애너테이션 매개변수의 값을 추출하여 테스트 메서드가 올바른 예외를 던지는지 확인한다.
- 매개변수 타입을 <? extends Throwable> 와일드 카드 타입으로 선언했다.
- 배열을 사용하여 여러 value 를 받는 Annotation
@Retetion(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface ExceptionTest { Class<? extends Throwable>[] value(); }
@ExceptionTest( {ArithmeticException.class, IndexOutOfBoundsException.class} ) public static void test() {...}
- @Repeatable을 사용한 annotation (반복 가능 Annotation)
- Java 8 부터는 여러개의 매개변수를 받는 방법을 배열로 선언하는 것이 아닌 @Repeatable 메타 애너테이션을 사용하여 할 수 있다.
- 컨테이너 애너테이션을 하나 더 정의하여 사용한다. ```java @Retetion(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @Repeatable(ExceptionTestContainer.class) public @interface ExceptionTest { Class<? extends Throwable> value(); }
@Retetion(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface ExceptionTest { ExceptionTest[] value(); }
```java @ExceptionTest(IndexOutOfBoundsException.class) @ExceptionTest(NullPointerException.class) public static void doublyBad() { ... }
- 반복 가능 Annotation을 처리할 때에는 하나만 달았을 때와 구분하기 위해 컨테이너 애너테이션 타입이 적용된다.
- getAnnotationByType 메서드는 이 둘을 구분하지 않아 @ExceptionTest, @ExceptionTestContainer를 모두 가져온다.
- isAnnotationPresent는 이 둘을 구분한다.
- 만약 @ExceptionTest를 여러번 달면 m.isAnnotationPresent(ExceptionTest.class)는 false
- 만약 @ExceptionTest를 한번 달면 m.isAnnotationPresent(ExceptionTestContainer.class)는 false
- 결론
- 애너테이션으로 할 수 있는 일을 명명 패턴으로 처리할 이유는 없다.
- 매개변수를 받지 않는 @Test Annotation
ITEM 40. @Override 애너테이션을 일관되게 사용하라.
- @Override
- 메서드 선언에만 달 수 있으며, 이 Annotation을 일관되게 사용하여 상위 타입의 메서드를 재정의했음을 뜻한다.
- 이 애너테이션을 일관되게 사용하면 여러 가지 악명 높은 버그를 예방해준다.
- 예제
public boolean equals(Bigram b) { return b.first == this.first && b.second == this.second; } public int hashCode() { return 31 * this.first + this.second; }
- equals를 재정의(overriding)한 것이 아닌 다중정의를 했다(overloading)
- 만약 @Override를 붙였다면 파라미터에 타입이 다르다는 것을 컴파일러가 알려준다.
- 예외는 구체 클래스에서 상위 클래스의 추상 메서드를 재정의한 경우에는 이 애너테이션을 달지 않아도 된다.
@Override public boolean equals(Object o) { if( !(o instanceof Bigram) ) return false; Bigram bigram = (Bigram)o; return bigram.first == first && bigram.second == second; }
ITEM 41. 정의하려는 것이 타입이라면 마커 인터페이스를 사용하라.
- 마커 인터페이스
- 자신을 구현하는 클래스가 특정 속성을 가짐을 표시해주는 인터페이스
- 예시) Serializable 인터페이스는 자신을 구현한 클래스의 인스턴스는 ObjectOutputStream을 통해 쓸(write)수 있다고 즉, 직렬화(serialization)할 수 있다고 알려준다.
- 마커 인터페이스의 장점(vs 마커 애너테이션)
- 마커 인터페이스는 이를 구현한 클래스의 인스턴스들을 구분하는 타입으로 쓸 수 있으나, 마커 애너테이션은 그렇지 않다.
- 마커 인터페이스 또한 타입이기 때문에 런타임에만 발견될 오류를 컴파일 타임에 잡을 수 있다.
- 적용 대상을 더 정밀하게 지정할 수 있다는 것이다.
- 마커 애노테이션은 @Target ElementType.TYPE으로 선언한 애너테이션은 모든 타입(클래스, 인터페이스, 열거 타입, 애너테이션)에 달 수 있다.
- 마커 인터페이스는 이를 구현한 클래스의 인스턴스들을 구분하는 타입으로 쓸 수 있으나, 마커 애너테이션은 그렇지 않다.
- 결론
- 클래스와 인터페이스 외의 프로그램 요소를 마킹해야 할 때 애너테이션을 쓸 수밖에 없다.
- 다만 마킹이 된 객체를 매개변수로 받는 메서드를 작성할 일이 있는 경우 마커 인터페이스를 사용하는 편이 좋다.