이펙티브자바, 35-38. 열거 타입 사용에서의 팁들, ordinal(), EnumSet, EnumMap

35. ordinal 메서드 대신 인스턴스 필드를 사용하라

  • enum은 ordinal()을 제공하며, 이것은 각각의 상수를 숫자로 관리하는 기능이다.
  • 이 기능은 상수의 변화에 따라 가변적이기 때문에 사용해서는 안된다.
  • 만약 정수를 사용하고 싶다면 ordinal()이 아닌 필드를 사용해야 한다.
public enum OrdinalEnum {
    // APPLE, CARROT;
    GRAPH, APPLE, CARROT;

    public static void main(String[] args) {
        System.out.println("OrdinalEnum.APPLE.ordinal() = " + OrdinalEnum.APPLE.ordinal()); // 0 -> 1
    }
}
  • APPLE, CARROT 만 있었을 때 아래의 값은 0이었다.
  • GRAPH가 추가되면 아래의 값은 1이 된다.
public enum OrdinalField {
    APPLE(1), CARROT(2);

    private final int intType;

    OrdinalField(int intType) {
        this.intType = intType;
    }

    int intType(){
        return intType;
    }

    public static void main(String[] args) {
        System.out.println("OrdinalField.APPLE.intType(); = " + OrdinalField.APPLE.intType());
    }
}
  • 위의 방식을 할 경우 항상 필드값을 가져오기 때문에 상수의 변화와 관계 없이 1을 리턴한다.
  • 실제로 ordinal()은 열거타입을 위한 자료구조를 위한 메서드이며, 개발자는 사용할 일이 없다.

36. 비트 필드 대신 EnumSet을 사용하라

  • 상수를 집합으로 사용할 때, 이전에는 2의 거듭제곱을 할당한 방식을 사용했다.
  • 비트로 할 필요가 없이 EnumSet으로 처리한다.
public class Text {
    public enum Style {BOLD, ITALIC, UNDERLINE, STRIKETHROUGH}

    public void applyStyles(Set<Style> styles) {
        for (Style style : styles) {
            System.out.printf("적용할 스타일은 %s 입니다.\n", style.toString());
        }
    }

    public static void main(String[] args) {
        Text text = new Text();
        text.applyStyles(EnumSet.of(Style.BOLD, Style.UNDERLINE));
    }
}

37. ordinal 인덱싱 대신 EnumMap을 사용하라

  • 아래는 Plant 클래스이며, 각 식물의 생애주기(LifeCycle)을 enum으로 표현한다.
  • Plant의 속성으로 생애주기를 보고 싶지만, 생애주기를 기준으로 Plant를 분류하고 싶을 수 있다.
  • 이런 경우 아래의 테스트와 같이 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;
    }
}
@Test
void test1()
    Plant[] garden = {
            new Plant("바질",    LifeCycle.ANNUAL),
            new Plant("캐러웨이", LifeCycle.BIENNIAL),
            new Plant("딜",      LifeCycle.ANNUAL),
            new Plant("라벤더",   LifeCycle.PERENNIAL),
            new Plant("파슬리",   LifeCycle.BIENNIAL),
            new Plant("로즈마리", LifeCycle.PERENNIAL)
    };

    final EnumMap<LifeCycle, Set<Plant>> plantsByLifeCycle = new EnumMap<>(LifeCycle.class);

    // EnumMap 을 초기화 한다.
    for(Plant.LifeCycle lc : Plant.LifeCycle.values()){
        plantsByLifeCycle.put(lc, new HashSet<>());
    }

    // 각 Plant 객체를 삽입한다.
    for(Plant p : garden){
        plantsByLifeCycle.get(p.lifeCycle).add(p);
    }

    for (LifeCycle lifeCycle : plantsByLifeCycle.keySet()) {
        System.out.println("lifeCycle = " + lifeCycle);
        System.out.println("plantsByLifeCycle.get(lifeCycle) = " + plantsByLifeCycle.get(lifeCycle));
        System.out.println();
    }
}
  • 위의 방식이 아닌 Stream을 활용할 수 있으며 예제는 아래와 같다.
  • 전자는 단순한 HashMap이며 후자는 EnumMap을 사용한다.
  • 위의 방식와 스트림의 방식은 동작이 다소 다르다. 전자는 모든 이넘이 키로 있다면 후자는 값이 없는 이넘에 대해서 키가 존재하지 않는다.
final Map<LifeCycle, List<Plant>> plantsByLifeCycle1 = Arrays.stream(garden)
        .collect(groupingBy(p -> p.lifeCycle));

final EnumMap<LifeCycle, Set<Plant>> plantsByLifeCycle2 = Arrays.stream(garden)
        .collect(groupingBy(
                p -> p.lifeCycle
                , () -> new EnumMap<>(LifeCycle.class)
                , toSet()));