자바에서 파이썬의 defaultdict 처럼 초기값을 가지는 Map 사용하기

sojukang·2022년 2월 22일
0

결론

MapputIfAbsentgetOrDefault를 사용하면 자바의 Map도 파이썬의 defaultDict 처럼 초기값을 가지듯 사용할 수 있다.

마주한 상황

아이고~ 어째서 자바에는 초기값을 가지는 Map이 없는걸까? 우리는 Collection을 순회하며 value의 초기 값이 0이고 key와 동일한 값이 나올 때마다 value를 1씩 증가시키는 Map을 원했다.

public static void main(String[] args) {
	Map<String, Integer> map = new HashMap<>();
    List<String> strings = Arrays.asList("ok", "ok", "ok", "no");

	for (String string : strings) {
		map.put(string, map.get(string) + 1); // 여기서 NPE
	}

	System.out.println(map.entrySet());
Exception in thread "main" java.lang.NullPointerException

어림도 없다. 결과는 NPE다. 당연하다. 아직 존재하지 않는 key를 참조했으니까... 아니, 파이썬의 DefaultDict 처럼 특정 값으로 초기화된 Map을 사용할수는 없는 걸까??
우리의 자바, 그런 건 없으시다. 그래도 직접 만들어볼 수는 있다. 좀 복잡하게...

defaultDict을 직접 구현해본다면?

public class DefaultDict<K, V> extends HashMap<K, V> {
    Class<V> klass;
    
    public DefaultDict(Class klass) {
        this.klass = klass;    
    }

    @Override
    public V get(Object key) {
        V returnValue = super.get(key);
        if (returnValue == null) {
            try {
                returnValue = klass.newInstance();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
            this.put((K) key, returnValue);
        }
        return returnValue;
    }    
}

이렇게까지 필요할까? 필요한 기능에 비해 과한 코드가 필요한듯 하다. GuavaForwardingMap을 상속해서 구현해볼 수도 있다.

public class DefaultMap<K, V> extends ForwardingMap<K, V> {
	private final Map<K, V> delegate;
    private final Supplier<V> defaultSupplier;

  /**
   * Creates a map which uses the given value as the default for <i>all</i>
   * keys. You should only use immutable values as a shared default key.
   * Prefer {@link #create(Supplier)} to construct a new instance for each key.
   */
  	public static DefaultMap<K, V> create(V defaultValue) {
    	return create(() -> defaultValue);
  	}

  	public static DefaultMap<K, V> create(Supplier<V> defaultSupplier) {
    	return new DefaultMap<>(new HashMap<>(), defaultSupplier);
  	}

  	public DefaultMap<K, V>(Map<K, V> delegate, Supplier<V> defaultSupplier) {
    	this.delegate = Objects.requireNonNull(delegate);
    	this.defaultSupplier = Objects.requireNonNull(defaultSupplier);
  	}

  	@Override
  	public V get(K key) {
    	return delegate().computeIfAbsent(key, k -> defaultSupplier.get());
  	}
}

Map이 제공하는 putIfAbsent와 getOrDefault를 사용하면 된다.

쓰읍... 역시 직접 사용하기엔 조금 아닌 것 같다. 그냥 keynull일 때만 초기값을 넣어주거나 반환해줄 수 있으면 간단하게 구현할 수 있을 것 같다. 제네릭으로 Mapput이나 get을 오버라이딩해서 사용하기에는... 역시 수많은 메소드를 오버라이딩하는게 복잡하다. 그런데 처음 코드를 조금만 수정하면 목적을 달성할 수 있었다.

public static void main(String[] args) {
	Map<String, Integer> map = new HashMap<>();
	List<String> strings = Arrays.asList("ok", "ok", "ok", "no");
	int defaultValue = 0;

	for (String string : strings) {
		map.put(string, map.getOrDefault(string, defaultValue) + 1); 
         // map.putIfAbsent(string, defaultValue); 동일한 결과를 얻는다.
         // map.merge(string, 1, Integers::sum); 역시 동일한 결과를 얻는다.
	}

	System.out.println(map.entrySet());
}
[no=1, ok=3]

putIfAbsentkey에 매칭되는 valuenull일 때 value에 넣을 값을 지정해줄 수 있다. getOrDefaultkey에 매칭되는 valuenull일 때 지정해준 defaultValue를 반환한다. 이 두 기능을 사용하면 자바의 Map도 파이썬의 defaultdict처럼 사용할 수 있다.
mergekey에 매칭되는 valuenull이거나 아니거나 상관 없이 동작하며, 현재 value에 1을 더한 값을 저장한다. null일 경우 1이 저장된다.

참고

stackoverflow defaultdict 관련 글

profile
기계공학과 개발어린이

0개의 댓글