티스토리 뷰

// 식물을 아주 단순하게 표현한 클래스 (226쪽)
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;
    }
}

 

식물을 표현하는 클래스이다. 식물들 마다 한해살이, 여러해살이, 두해살이와 같은 생애 주기가 존재한다.

 

 

 

 public static void main(String[] args) {
        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)
        };

        // 코드 37-1 ordinal()을 배열 인덱스로 사용 - 따라 하지 말 것! (226쪽)
        Set<Plant>[] plantsByLifeCycleArr =
                (Set<Plant>[]) new Set[Plant.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]);
        }
}

 

식물들을 생애주기에 따라 집합으로 표현하는 코드이다. 코드의 진행 순서는 다음과 같다.

 

  1. vlaues() -> 상수 배열을 가져오고 -> length로 배열의 길이를 가져옴 -> 배열 길이와 같은 Set 배열을 생성함

  2. Set 배열은 null로 초기화 돼 있으니 Set 구현체로 초기화해 준다.

  3. garden을 순회하며 Plant의 LifeCycle의 ordinal()로 Set 배열의 인덱스를 가져오고 해당 인덱스의 Set에 Plant를 추가한다. Plant의 hashcode를 구현하면 고유한 Plant만 Set에 추가할 수 있다.

 

이를 통해 한해살이, 여러해살이, 두해살이에 해당하는 식물들로 분류를 할 수 있다.

 

위와 같은 코드의 문제점을 길게 설명하고 있지만 요약하면 "자료구조 만들어 놨으니 직접 만들지 마라. 너 실수할 거잖아 ㅋㅋ"이다.

 

 

 

// 코드 37-2 EnumMap을 사용해 데이터와 열거 타입을 매핑한다. (227쪽)
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);

LifeCycle을 key로 Plant를 담은 Set을 value로 하는 EnumMap이다. 만들어 준 자료구조를 활용하면 코드를 손쉽게 작성할 수 있다.

 

 

아니 근데 ordinal()로 직접 만들면 성능상 유리하잖아요?

아니다. EnumMap은 HashMap처럼 hashcode를 사용하는 것이 아닌 위에 직접 자료구조를 만들었던 것처럼 ordinal()과 배열을 통한 인덱싱으로 구현돼 있다.

생성자 하나랑 put method를 가져와봤다. enum의 상수들은 컴파일 타임에 제일 먼저 초기화된다. getKeyUniverse()를 통해 상수 배열을 가져오고 상수 배열과 같은 크기의 Object 배열을 생성한다. ordinal() 단순 인덱싱을 통해 value를 삽입할 수 있게 된다.

 

 

이 외에도 편리한 Map의 method들을 구현해 두었다. 자료구조 이렇게 짤 자신 없으면 EnumMap 써야 할 것 같다.

 

 

 

// 코드 37-3 스트림을 사용한 코드 1 - EnumMap을 사용하지 않는다! (228쪽)
System.out.println(Arrays.stream(garden)
        .collect(groupingBy(p -> p.lifeCycle)));

// 코드 37-4 스트림을 사용한 코드 2 - EnumMap을 이용해 데이터와 열거 타입을 매핑했다. (228쪽)
System.out.println(Arrays.stream(garden)
        .collect(groupingBy(p -> p.lifeCycle,
                () -> new EnumMap<>(LifeCycle.class), toSet())));

 

스트림을 이용해 자료구조 초기화를 간소화할 수 있다. 그러나 코드 1은 groupingBy를 통해 기본 Map 구현체로 초기화된다. EnumMap이 아니면 Enum의 hashcode (Object의 hashcode를 그대로 씀. 상수들은 각각 싱글톤 인스턴스이기 때문에 레퍼런스 기반 해쉬코드를 써도 됨)를 통해 인덱싱을 하게 되는데 이러면 배열의 크기가 커진다. Enum의 상수들은 연속된 배열로 관리되기 때문에 기본 Map을 사용할 필요가 없다.

 

 

코드 2를 통해 Map의 구현체를 EnumMap으로 표현할 수 있다.

위와 같이 스트림을 통해 초기화를 하면 차이점은 garden안에 LifeCycle이 두 개 밖에 없으면 EnumMap안에 key가 두개 생긴다고 한다. 근데 사실 이 부분은 이해가 안 된다. EnumMap의 생성자에 enum class를 넣는 것은 위의 사진에 첨부했다. enum의 상수배열을 통해 배열을 초기화하는데 스트림은 왜 두 개만 생기는지 모르겠다. 이건 스트림의 동작 방식을 공부해야 알 수 있을 것 같다. 45장까지 숨참고 읽어야겠다.

 

 

 

 

 

 

고체, 액체, 기체가 있고 이 상태의 전이 과정을 enum으로 표현하고자 하면 아래와 같이 작성할 수 있다.

public enum Phase {
    SOLID, LIQUID, GAS;
    public enum Transition {
        MELT, FREEZE, BOIL, CONDENSE, SUBLIME, DEPOSIT;
        
        private static final Transition[][] TRANSITIONS = {
            {null, MELT, SUBLIME },
            {FREEZE, null, BOIL},
            {DEPOSIT, CONDENSE, null}
       };

       // 한 상태에서 다른 상태로의 전이를 반환
       public static Transition from(Phase from, Phase to){
        return TRANSITIONS[from.ordinal()][to.ordinal()];
       }
   }
}

 

쉽게 말해 Phase를 그래프로 만든 것이다. SOLID에서 LIQUID는 MELT... 이를 각각 상태의 인덱스를 통해 그래프 화했다.

 

위의 코드의 문제점은 다음과 같다.

  1. 코드 짜다 실수할 수 있다.

  2. 상태관계를 인접 행렬로 표현해서 null이 마구마구 들어간다.

  3. 유지보수가 끔찍하다. Phase에 PLASMA 상태를 추가하면 배열을 늘리고, 상태 관계를 표현해야 한다.
    이 과정에서 1, 2번 문제가 발생할 수 있다.

 

EnumMap을 사용한 개선이다.

// 코드 37-6 중첩 EnumMap으로 데이터와 열거 타입 쌍을 연결했다. (229-231쪽)
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;
        }

        // 상전이 맵을 초기화한다.
        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);
        }
    }
}

다른 코드들은 쉽지만 Map의 초기화 코드가 복잡하다. 흐름은 아래와 같다.

 

  1. Stream.of(values()) -> Transition의 values method를 통해 상수 배열을 가져온다. (MELT, FREEZE, BOIL...)

  2. groupingBy(t -> t.from, () -> new EnumMap<>(Pahse.class)) -> Transition의 from을 key로 그룹핑한다. 구현체는 EnumMap으로 -> EnumMap이 성능상 유리하다.

  3. from을 key로 하는 EnumMap의 Value는 무엇을 쓸까? Transition의 to를 key로, Transition을 value로 하는 EnumMap이 들어간다.

 

이렇게 되면 from -> to를 통해 Transition을 반환하는 중첩 EnumMap을 생성할 수 있다.

 

 

toMap안의 (x, y) -> y는 어디 쓰이나요?

안 쓰인다.

Collectors의 toMap method이다. 점층적 factory method로 구현돼 있다. 다른 toMap을 사용하면 Map의 구현체가 HashMap이 들어간다. 명시적으로 EnumMap을 넣어주기 위해 가운데 BinaryOperator를 받는 method를 사용했다.

 

 

 

결론 : 실수하지 말고 만들어 준 EnumMap 사용하자.

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
글 보관함