이펙티브 자바 -5-
열거 타입과 애너테이션
자바에는 특수한 목적의 참조 타입이 두 가지 있다. 하나는 클래스 일종인 열거(enum)타입이고 하나는 인터페이스 일종인 애너테이션(annotation)이다. 이 타입을 올바르게 사용하는 방법을 알자
열거 타입은 일정 개수의 상수 값을 정의한 다음, 그 외의 값을 허용하지 않는 타입이다. 열거 타입 자체는 클래스이며, 상수 하나당 자신의 인스턴스를 하나씩 만들어 public static final 필드로 공개한다. 열거 타입은 밖에서 접근할 수 있는 생성자를 제공하지 않으므로 사실상 final이다. 따라서 클라이언트가 인스턴스를 직접 생성하거나 확장할 수 없어 열거 타입 선언으로 만들어진 인스턴스들은 딱 하나씩만 존재하는 것이 보장된다.
int 상수 대신 열거 타입을 사용하라
정수형 열거 패턴은 단점이 많다 타입 안정을 보장할 수 있는 방법이 없으며 표현력도 좋지 않다
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;
orange를 건네야 할 메소드에 apple을 건네도 int type이기 때문에 타입 안정이 보장되지 않는다. 이는 프로그램에서 에러를 일으키기 쉽고 엉뚱하게 동작할 가능성이 생긴다,
열거 타입
public static enum Apple{a1,a2}; //a1,a2는 Apple.class
public static enum Orange{a1,a2}; //a1,a2는 Orange.class
열거 타입은 컴파일 타임에 타입 안전성을 제공한다. 다른 타입의 값을 넘기려 하면 컴파일 오류가 난다.
//데이터와 메서드를 같는 열거 타입
public static enum Apple{
// 열거할 type 앞에 나와야 한다
a1(1,2) // Apple.class 내부적으로 Apple 인스턴스 생성
,a2(2,3);
private final int a;
private final int b;
// private default 생성자
Apple(int a, int b) {
this.a = a;
this.b = b;
}
public int getA() {
return a;
}
public int getB() {
return b;
}
};
열거 타입 상수 각각을 특정 데이터와 연결지으려면 생성자에서 데이터를 받아 인스턴스 필드에 저장하면 된다.
상수별 클래스 몸체와 데이터를 사용한 열거 타입
public enum Operation {
// 각각이 열거 상수 인스턴스가 apply 함수를 구현하였다
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);
}
열거타입의 정적 필드 중 열거 타입의 생성자에서 접근할 수 있는 것은 상수 변수 뿐이다.
따라서 열거 타입의 생성자가 실행하는 시점에 정적 필드들이 초기화 전이기 때문에 자기 자신을 추가하지 못하게 하는 제약이 필요하다.(열거 타입 상수는 생성자에서 자신의 인스턴스를 맵등에 추가할 수 없다)
또한 열거 타입 생성자에서 같은 열거 타입의 다른 상수에도 접근할 수 없다.
전략 열거 타입 패턴
enum PayrollDay {
MONDAY(PayType.WEEKDAY), TUESDAY(PayType.WEEKDAY), WEDSDAY(PayType.WEEKDAY),
THURSDAY(PayType.WEEKDAY), FRIDAY(PayType.WEEKDAY),
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 minusWorked, int payRate) {
return minusWorked <= MINS_PER_SHIFT ? 0 :
(minusWorked - MINS_PER_SHIFT) * payRate / 2;
}
},
WEEKEND {
int overtimePay(int minusWorked, int payRate) {
return minusWorked * 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);
}
}
새로운 상수를 추가할 때 잔업수당 '전략'을 선택하도록 하는 것이다. 잔업 수당 계산을 private 중첩 열거 타입(PayType)으로 옮기고, PayrollDay 열거 타입의 생성자에서 이 중 적당한 것을 선택한다. 그러면 PayrollDay 열거 타입은 PayType에게 잔업 수당 계산을 위임하여 최종 계산을 수행한다. 이 경우 switch문이나 상수별 메서드 구현이 필요없게 되므로 더 안전하고 유연하다
switch 문을 이용해 원래 열거 타입에 없는 기능을 수행한다
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);
}
}
switch 문은 열거 타입의 상수별 동작을 구현하는 데 적합하지 않지만 기존 열거 타입에 상수별 동작을 혼합해 넣을 때는 좋은 선택이다
정리
필요한 원소를 컴파일 타입에 다 알 수 있는 상수 집합이라면 항상 열거 타입을 사용하자. 열거 타입은 확실히 정수 상수보다 뛰어나다. 더 읽기 쉽고 타입 안전하다.
ordinal 메서드 대신 인스턴스 필드를 사용하라
모든 열거 타입은 해당 상수가 그 열거 타입에서 몇 번째 위치인지 반환하는 ordinal이라는 메서드를 제공한다. ordinal에 의존하면 상수 선언 순서를 바꾸는 순간 오작동할 수 있다. 따라서 열거 타입 상수에 연결된 값은 ordinal 메서드로 얻지 말고 인스턴스 필드에 저장하자
비트 필드 대신 EnumSet을 사용하라
열거한 값들이 주로 집합으로 사용되는 경우, 예전에는 비트필드(각 상수에 서로 다른 2의 거듭 제곱 값을 할당한 정수 열거 패턴)을 사용했다. 이 방법은 비트 연산자를 사용해서 합집합과 교집합과 같은 집합 연산을 효율적으로 수행할 수 있다.
하지만 비트 필드를 사용하게 되면 정수 열거 상수의 단점을 가질 뿐만이 아니라 해석하기 어렵다는 단점이 있다. java.util 패키지의 EnumSet 클래스는 열거 타입 상수의 값으로 구성된 집합을 효율적으로 표현해 줄 뿐만 아니라 EmumSet의 내부는 비트 백터로 구현되어 있어 비트 필드와 같은 성능을 보여준다.
사용 예시
public class Text {
public enum Style {BOLD, ITALIC, UNDERLINE, STRIKETHROUGH}
// 어떤 Set을 넘겨도 되나, EnumSet이 가장 좋다.
public void applyStyles(Set<Style> styles) {
System.out.printf("Applying styles %s to text%n",
Objects.requireNonNull(styles));
}
// 사용 예
public static void main(String[] args) {
Text text = new Text();
text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));
}
}
정리
열거할 수 있는 값을 집합으로 사용하는 경우 비트 필드를 사용하면 효율적으로 집합 연산을 수행할 수 있다. 하지만 정수 열거 패턴의 문제점과 해석하기 어렵다. 이러한 문제를 개선한 EnumSet 클래스를 사용하자
ordinal 인덱싱 대신 EnumMap을 사용하라
목표 : 식물 list를 type에 따라 분류하자
// Plant 이라는 list를 담는 class 가 있다고 가정
List<Plant> list = new ArrayList<...>
// 생명 type
enum LifeCycle{
ANNUL, PERENNIAL, BIENNIAL...
}
class Plant{
// 식물 이름
final String name;
// 식물 생명 type
final LifeCycle lifeCycle;
}
안 좋은 방식 - 따라 하지 말 것
Set<Plant>[] plantsByLifeCycleArr =
(Set<Plant>[]) new Set[LifeCycle.values().length];
for (int i = 0; i < plantsByLifeCycleArr.length; i++)
plantsByLifeCycleArr[i] = new HashSet<>();
for (Plant p : garden)
plantsByLifeCycleArr[p.lifeCycle.ordinal()].add(p);
// 결과 출력
for (int i = 0; i < plantsByLifeCycleArr.length; i++) {
System.out.printf("%s: %s%n",
Plant.LifeCycle.values()[i], plantsByLifeCycleArr[i]);
}
문제점 -정수값(oridinal)을 사용하였다. 정수 값은 열거 타입과 달리 타입에 안전하지 않다.(ordinal은 상수 선언 순서를 바꾸면 바뀐다). 따라서 우리는 정확한 정숫값을 사용한다는 것을 우리가 책임을 져야 하는 단점을 지닌다. 이는 오류가 나기 쉽다.
개선 방식 - EnumMap을 사용해 데이터와 열거 타입을 매핑
Map<LifeCycle, Set<Plant>> plantsByLifeCycle =
new EnumMap<>(LifeCycle.class);
for (LifeCycle lc : LifeCycle.values())
plantsByLifeCycle.put(lc, new HashSet<>());
for (Plant p : garden)
plantsByLifeCycle.get(p.lifeCycle).add(p);
System.out.println(plantsByLifeCycle);
짧고 명료하고 안전하며, EnumMap도 내부에서 배열을 사용하기 때문에 성능도 비슷하다
개선 방식 - stream 추가
//Enummap 사용 안하는 경우
// 최적화 안된다
list.stream().collect(groupBy(Plant::getLifeCycle));
//Enummap 사용 하는 경우
// 최적화 된다
list.stream().collect(groupBy(Plant::getLifeCycle,() -> new EnumMap<>(LifeCycle.class), toSet()));
정리
배열의 인덱스를 얻기 위해 ordinal을 쓰는 것은 일반적으로 좋지 않으니. 대신 EnumMap을 사용해라
확장할 수 있는 열거 타입이 필요하면 인터페이스를 사용하라
열거 타입 자체는 확장할 수 없지만, 인터페이스와 그 인터페이스를 구현하는 기본 열거 타입을 함께 사용하면 같은 효과를 낼 수 있다. 이렇게 하면 사용자는 이 인터페이스를 구현해 새로운 열거 타입을 만들 수 있다. 새로운 열거 타입이 인터페이스 기반으로 작성되어 있다면, 기본 열거 타입에 쓰이는 모든 곳에 새로운 열거 타입을 사용할 수 있다.
명령 패턴보다 애너테이션을 사용하라
전통적으로 두구나 프레임워크가 특별히 다뤄야 할 프로그램 요소에는 구분되는 명명 패턴을 적용했다. 예를 들어 junit 버전 3의 경우 테스트 메서드 이름을 test로 시작하게 했다. 이는 효과적인 방법이지만 단점도 크다
명령패턴 단점
- 오타가 나면 안된다 - 명명 패턴과 맞지 않는 경우 무시한다
- 올바른 프로그램 요소에서만 사용되리라 보장할 방법이 없다
- 프로그램 요소를 매개변수로 전달할 마땅한 밥법이 없다.
애너테이션은 이러한 문제를 해결해준다. 애너테이션은 클래스에 직접적인 영향을 주지를 않으면서 그 애너테이션에 관심이 있는 프로그램에 추가적인 정보를 제공한다. 즉, 대상 코드의 의미는 그대로 둔 채 그 애너테이션에 관심 있는 도구에서 특별한 처리를 할 기회를 준다
* @Repeatable 애너테이션
@Repeatable 사용 시 주의점
- @Repeatable을 명시한 애너테이션을 반환하는 컨테이너 애너테이션을 하나 더 정의해야 한다. 그리고 @Repeatable에 해당 컨테이너 애너테이션의 class 객체를 매개변수로 전달해야 한다.
- 컨테이너 애너테이션은 내부 애너테이션 타입의 배열을 반환하는 value 메서드를 정의해야 한다.
- 적절한 @Retention과 @Target을 명시해야 한다. 그렇지 않으면 컴파일되지 않는다.
아래의 코드는 @Repeatable 메타 애너테이션을 적용한 ExceptionTest 애너테이션 선언부를 나타낸다.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(ExceptionTestContainer.class)
public @interface ExceptionTest {
Class<? extends Throwable> value();
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTestContainer {
ExceptionTest[] value();
}
적용 방법
@ExceptionTest(IndexOutOfBoundsException.class)
@ExceptionTest(NullPointerException.class)
public static void doublyBad() { }
@ExceptionTest를 여러 개 달면 하나만 달렸을 때와 구분하기 위해 컨테이너 애너테이션 타입이 적용된다. getAnnotaionByTpye 메서드는 이 둘을 구분하지 않아서 반복 가능 애너테이션과 그 컨테이너 애너테이션을 모두 가져온다. 반면에 isAnnotationPresent는 이 둘을 명확히 구분한다.
@ExceptionTest를 여러 개 달 경우 ExceptionTestContainer.class가 적용된다 따라서 isAnnotationPresent(ExceptionTest.class) = false다
@ExceptionTest를 하나만 달 경우 ExceptionTest.class가 적용된다 따라서 isAnnotationPresent(ExceptionTest.class) = true다
* 애너테이션 선언에 다른 애너테이션을 메타 애너테이션이라 한다
정리
애너테이션으로 처리할 수 있다면 명명 패턴을 사용할 이유는 없다. 애너테이션 기능을 사용해서 소스 가독성을 높일 수 있다.
@Override 애너테이션을 일관되게 사용하라
@Overrided는 매서드 선언에만 달 수 있으며, 이 애너테이션이 달렸다는 것은 상위 타입의 메서드를 재정의 했음을 뜻한다. 이 애너테이션을 일관되게 사용하면 버그들을 예방해준다. 애너테이션을 사용하면 컴파일러가 잘못된 부분을 명확히 알려주므로 올바르게 수정할 수 있다
상위 클래스의 메서드를 재정의 하려는 모든 메서드에 @Override 애너테이션을 달자. 예외는 구체 클래스에서 상위 클래스의 추상 메서드를 정의할때는 굳이 @Override를 달지 않아도 된다. 구체 클래스인데 아직 구현하지 않은 추상 메서드가 남아있다면 컴파일러가 알려주기 때문이다. (하지만 단다고 해로운 것이 아니므로 필요하면 달자)
정리
재정의한 모든 메서드에 @Override 메서드를 의식적으로 달면 우리가 실수했을 때 컴파일러를 통해 오류를 쉽게 바로 잡을 수 있다.
정의하려는 것이 타입이라면 마커 인터페이스를 사용하라
아무 메서드도 담고 있지 않고, 단지 자신을 구현하는 클래스가 특정 속성을 가짐을 표현해주는 인터페이스를 마커 인터페이스라고 한다. 대표적으로 Serializable 인터페이스가 있다. Serializable은 자신을 구현한 클래스의 인스턴스는 ObjectOutputStream을 통해 쓸 수 있다고, 즉 직렬화 할 수 있다고 알려준다.
마커 인터페이스는 마커 애너테이션보다 나은 장점을 가진다.
마커 인터페이스는 이를 구현한 클래스의 인스턴스들을 구분하는 타입으로 쓸 수 있으나, 마커 애너테이션은 그렇지 않다. 마커 인터페이스는 어엿한 타입이기 때문에, 마커 애너테이션을 사용했다면 런타임에야 발견된 오류를 컴파일 타임에 잡을 수 있다
반면 마커 애너테이션이 마커 인터페이스보다 나은 점은 거대한 애너테이션 시스템의 지원을 받는다.
정리
마커 인터페이스와 마커 애너테이션은 각자의 쓰임이 있다.
새로 추가하는 메서드 없이 단지 타입 정의가 목적이라면 마커 인터페이스를 선택하자.
클래스나 인터페이스 외의 프로그램 요소에 마킹해야 하거나, 애너테이션을 적극 활용하는 프레임워크의 일부로 마커를 편입하고 싶다면 마커 애너테이션을 사용하자