열거 타입은 일정 개수의 상수 값을 정의한 다음, 그 외의 값은 허용하지 않는 타입이다.
// 가장 단순한 열거 타입
public enum Apple { FUJI, PIPPIN, GRANNY_SMITH }
public enum Orange { NAVEL, TEMPLE, BLOOD }
자바 열거 타입을 뒷받침하는 아이디어는 단순하다. 열거 타입 자체는 클래스이며, 상수 하나당 자신의 인스턴스를 하나씩 만들어 public static final
필드로 공개한다. 열거 타입은 밖에서 접근할 수 있는 생성자를 제공하지 않으므로 사실상 final
이다.
⇒ 따라서 클라이언트가 인스턴스를 직접 생성하거나 확장할 수 없으니 열거 타입 선언으로 만들어진 인스턴스들은 딱 하나씩만 존재함이 보장된다.
열거 타입은 근본적으로 불변이라 모든 필드는 final
이어야 한다.
// 상수별 클래스 몸체와 데이터를 사용한 열거 타입
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);
}
// 전략 열거 타입 패턴
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)
{
return minsWorked <= MINS_PER_SHIFT = 8 * 60;
}
},
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 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; }
}
Enum
의 API 문서를 보면 ordinal
메서드에 대해 이렇게 쓰여 있다. “대부분 프로그래머는 이 메서드를 쓸 일이 없다. 이 메서드는 EnumSet
과 EnumMap
같이 열거 타입 기반의 범용 자료구조에 쓸 목적으로 설계되었다.” 따라서 이런 용도가 아니라면 ordianl
메서드는 절대 사용하지 말자.
( ordinal 메서드는 열거 타입 내 필드의 위치를 반환하는 메서드 )
// 비트 필드 열거 상수 - 구닥다리 기법!
public class Text {
public static final int STYLE_BOLD = 1 << 0;
public static final int STYLE_ITALIC = 1 << 1;
public static final int STYLE_UNDERLINE = 1 << 2;
public static final int STYLE_STRIKETHROUGH = 1 << 3;
// 매개변수 styles 는 0 개 이상의 STYLE_ 상수를 비트별 OR한 값이다.
public void applyStyles( int styles ) { ... }
}
다음과 같은 식으로 비트별 OR 를 사용해 여러 상수를 하나의 집합으로 모을 수 있으며, 이렇게 만들어진 집합을 비트 필드라 한다.
text.applyStyles( STYLE_BOLD | STYLE_ITALIC );
비트 필드를 사용하면 비트별 연산을 사용해 합집합과 교집합과 같은 집합 연산을 효율적으로 수행할 수 있다. 하지만 비트 필드는 정수 열거 상수의 단점을 그대로 지니며 추가로 다음과 같은 문제까지 안고 있다.
// EnumSet - 비트 필드를 대체하는 현대적 기법
public Class Text {
public enum Styel { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH }
// 어떤 Set을 넘겨도 되나, EnumSet 이 가장 좋다.
public void applyStyles( Set<Style> styles ) { ... }
}
text.applyStyles( EnumSet.of( Style.BOLD, Style.ITALIC ) );
💡 열거할 수 있는 타입을 한데 모아 집합 형태로 사용한다고 해도 비트 필드를 사용할 이유는 없다.
EnumSet
클래스가 비트 필드 수준의 명료함과 성능을 제공하고 아이템 34 에서 설명한 열거 타입의 장점까지 선사하기 때문이다.EnumSet
의 유일한 단점이라면 아직 불변EnumSet
을 만들 수 없다는 것이다.Collections.unmodifiableSet
으로EnumSet
을 감싸 사용할 수 있다.
// EnumMap 을 사용해 데이터와 열거 타입을 매핑한다.
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);
EnumMap
의 성능이 ordinal
을 쓴 배열에 비견되는 이유는 그 내부에서 배열을 사용하기 때문이다. 내부 구현 방식을 안으로 숨겨서 Map
의 타입 안전성과 배열의 성능을 모두 얻어낸 것이다.
여기서 EnumMap
의 생성자가 받는 키 타입의 Class
객체는 한정적 타입 토큰으로, 런타임 제네릭 타입 정보를 제공한다.
스트림을 사용해 맵을 관리하면 코드를 더 줄일 수 있다.
// 스트림을 사용한 코드 2 - EnumMap 을 이용해 데이터와 열거 타입을 매핑했다.
System.out.println(Arrays.stream(garden)
.collect(groupBy(p -> p.lifeCycle,
() -> new EnumMap<>(LifeCycle.class), toSet())));
스트림을 사용하면 EnumMap
만 사용했을 때와는 살짝 다르게 동작한다.
EnumMap
버전은 언제나 식물의 생애주기당 하나씩의 중첩 맵을 만들지만, 스트림 버전에서는 해당 생애주기에 속하는 식물이 있을 때만 만든다.
💡 배열의 인덱스를 얻기 위해
ordinal
을 쓰는 것은 일반적으로 좋지 않으니, 대신EnumMap
을 사용하라. 다차원 관계는EnumMap<…, EnumMap<…>>
으로 표현하라.
타입 안전 열거 패턴은 열거한 값들을 그대로 가져온 다음 값을 더 추가하여 다른 목적으로 쓸 수 있는 반면, 열거 타입은 그렇게 할 수 없다는 뜻이다.
기본 아이디어는 열거 타입이 임의의 인터페이스를 구현할 수 있다는 사실을 이용하는 것이다. 연산 코드용 인터페이스를 정의하고 열거 타입이 이 인터페이스를 구현하게 하면 된다.
// 확장 가능 열거 타입
public enum ExtededOperation implements Operation {
EXP("^") {
public double apply ( double x, double y ) {
return Math.pow( x, y );
}
},
REMINDER("%") {
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;
}
}
💡 열거 타입 자체는 확장할 수 없지만, 인터페이스와 그 인터페이스를 구현하는 기본 열거 타입을 함께 사용해 같은 효과를 낼 수 있다. API 가 인터페이스 기반으로 작성되었다면 기본 열거 타입의 인스턴스가 쓰이는 모든 곳을 새로 확장한 열거 타입의 인스턴스로 대체해 사용할 수 있다.
전통적으로 도구나 프레임워크가 특별히 다뤄야 할 프로그램 요소에는 딱 구분되는 명명 패턴을 적용해왔다.
예를 들어 테스트 프레임워크인 JUnit 3 까지 테스트 메서드 이름을 test 로 시작하도록 했다.
명명 패턴의 단점
// 마커 애너테이션 타입 선언
import java.lang.annotation.*;
// 테스트 메서드임을 선언하는 애너테이션이다.
// 매개변수 없는 정적 메서드 전용이다.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
}
위처럼 애너테이션 선언에 다는 애너테이션을 메타애너테이션이라 한다.
// 매개변수 하나를 받는 애너테이션 타입
import java.lang.annotation.*;
// 명시한 예외를 던져야만 성공하는 테스트 메서드용 애너테이션
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
Class<? extends Throwable> value();
}
// 배열 매개변수를 받는 애너테이션 타입
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
Class<? extends Throwable>[] value();
}
// 반복 가능 애너테이션
@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(ArithmeticException.class)
public static void method() { ... }
// 배열을 받는 애너테이션 사용법
@ExceptionTest( { ArithmeticException.class,
NullPointerException.class } )
public static void method() { ... }
// 반복 가능 애너테이션 사용법
@ExceptionTest(ArithmeticException.class)
@ExceptionTest(NullPointerException.class)
public static void method() { ... }
다른 프로그래머가 소스코드에 추가 정보를 제공할 수 있는 도구를 만드는 일을 한다면 적당한 애너테이션 타입도 함께 정의해 제공하자.
애너테이션으로 할 수 있는 일을 명명 패턴으로 처리할 이유는 없다.
자바 프로그래머라면 예외 없이 자바가 제공하는 애너테이션 타입들은 사용해야 한다.
상위 클래스의 메서드를 재정의하려는 모든 메서드에 @Override 애너테이션을 달자.
💡 재정의한 모든 메서드에
@Override
애너테이션을 의식적으로 달면 여러분이 실수했을 때 컴파일러가 바로 알려줄 것이다. 예외는 한 가지뿐이다. 구체 클래스에서 상위 클래스의 추상 메서드를 재정의한 경우엔 이 애너테이션을 달지 않아도 된다.
아무 메서드도 담고 있지 않고, 단지 자신을 구현하는 클래스가 특정 속성을 가짐을 표시해주는 인터페이스를 마커 인터페이스라 한다.
마커 인터페이스는 두 가지 면에서 마커 애너테이션보다 낫다.
반대로 마커 애너테이션이 마커 인터페이스보다 나은 점으로는 거대한 애너테이션 시스템의 지원을 받는다는 점이다.
💡 마커 인터페이스와 마커 애너테이션은 각자의 쓰임이 있다. 새로 추가하는 메서드 없이 단지 타입 정의가 목적이라면 마커 인터페이스를 선택하자. 클래스나 인터페이스 외의 프로그램 요소에 마킹해야 하거나, 애너테이션을 적극 활용하는 프레임워크의 일부로 그 마커를 편입시키고자 한다면 마커 애너테이션이 올바른 선택이다. 적용 대상이
ElementType.TYPE
인 마커 애너테이션을 작성하고 있다면, 잠시 여유를 갖고 정말 애너테이션으로 구현하는 게 옳은지, 혹은 마커 인터페이스가 낫지는 않을지 곰곰이 생각해보자.