[item 37] ordinal 인덱신 대신 EnumMap을 사용하라

김동훈·2023년 9월 10일
1

Effective-java

목록 보기
14/14

Ordinal 메서드로 인덱스 활용

Set<Plant>[] plantsByLifeCycle = (Set<Plant>[]) new Set[Plant.LifeCycle.values().length];
for(int i=0; i < plantsByLifeCycle.length; i++)
    plantsByLifeCycle[i] = new HashSet<>();
for (Plant p : garden)
    plantsByLifeCycle[p.lifeCycle.oridinal()].add(p);

ordinal() 메서드를 통해 인덱스로 활용하면 안된다고 말하고 있다. 그러면서 운이 좋으면 ArrayIndexOutOfBoundsException이 발생하거나, 잘못된 동작을 묵묵히 수행한다고 한다.
만약, Plant Enum에 상수가 하나 추가된다면, 그리고 그 상수의 ordinal에 접근하여 사용한다면ArrayIndexOutOfBoundsException이 발생하게 된다.
다른 상황으로는, Plant Enum 상수들의 순서가 바뀐경우 아무런 예외없이 잘못된 동작을 수행 할 것이다.

그래서 EnumMap을 사용하라고 전한다.

EnumMap 사용

        EnumMap<Plant.LifeCycle, Set<Plant>> plantsByLifeCycle = 
        new EnumMap<>(Plant.LifeCycle.class); // EnumMap + 한정적 타입 토큰 이용
        
        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을 사용하여 위 코드를 수정한 결과이다.
직접 index를 사용하지 않고, 관련 동작은 모두 Java 개발자에게 맡겼다. 또한, 성능도 좀 더 개선되었는데 내부에서 배열을 사용하기 때문이다. 즉, 성능+안정성을 얻어냈다.

주석을 보면 한정적 타입 토큰을 이용했다고 한다. EnumMap 생성자의 파라미터로 Class객체를 넘겨주는데 이는 중요한 역할을 한다. 내가 확인한바로는 3가지정도 역할이 있었다.

EnumMap생성자의 타입 토큰의 역할

1. 배열의 크기 설정

EnumMap은 내부적으로 배열을 이용한다고 했다. 배열을 생성 할 때 그 크기를 결정해줘야하는데, 여기에 사용된다.

public EnumMap(Class<K> keyType) {
        this.keyType = keyType;
        keyUniverse = getKeyUniverse(keyType);
        vals = new Object[keyUniverse.length];
    }

2. 타입 안전함 체크

한정적 타입 토큰의 본 역할을 수행한다.

public void putAll(Map<? extends K, ? extends V> m) {
        if (m instanceof EnumMap) {
            EnumMap<?, ?> em = (EnumMap<?, ?>)m;
            if (em.keyType != keyType) {
                if (em.isEmpty())
                    return;
                throw new ClassCastException(em.keyType + " != " + keyType);
            }

            for (int i = 0; i < keyUniverse.length; i++) {
                Object emValue = em.vals[i];
                if (emValue != null) {
                    if (vals[i] == null)
                        size++;
                    vals[i] = emValue;
                }
            }
        } else {
            super.putAll(m);
        }
    }

3. 성능 향상

가장 많이 사용할 get 메서드를 보면 isValidKey라는 메서드를 사용하고 있다.

public V get(Object key) {
        return (isValidKey(key) ?
                unmaskNull(vals[((Enum<?>)key).ordinal()]) : null);
    }
    
private boolean isValidKey(Object key) {
        if (key == null)
            return false;

        // Cheaper than instanceof Enum followed by getDeclaringClass
        Class<?> keyClass = key.getClass();
        return keyClass == keyType || keyClass.getSuperclass() == keyType;
    }

여기서 isValidKey 메서드의 Class<?> keyClass = key.getClass(); 라인의 주석에 힌트가 있다. 바로 리플렉션 메서드인 getDeclaringClass 보다 비용이 싸다는 장점이 있다. 일반적으로 리플렉션은 느리다고 알고 있다. 우리는 이미 EnumMap의 원소들의 타입 정보를 keyType을 통해 알고 있다. 따라서 리플렉션 메서드를 사용하지 않아도 클래스 타입을 비교할 수 있게된다.

만약 getDeclaringClass 메서드를 사용한다면 다음과 같은 형태일 것 같다.

Class<?> declaringClass = key.getClass().getDeclaringClass();
        if (keyType.isAssignableFrom(declaringClass)) {
            //
        }

참고


effective-java스터디에서 공유하고 있는 전체 item에 대한 정리글

profile
董訓은 영어로 mentor

0개의 댓글