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<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은 내부적으로 배열을 이용한다고 했다. 배열을 생성 할 때 그 크기를 결정해줘야하는데, 여기에 사용된다.
public EnumMap(Class<K> keyType) {
this.keyType = keyType;
keyUniverse = getKeyUniverse(keyType);
vals = new Object[keyUniverse.length];
}
한정적 타입 토큰의 본 역할을 수행한다.
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);
}
}
가장 많이 사용할 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에 대한 정리글