Map
의 putIfAbsent
과 getOrDefault
를 사용하면 자바의 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
을 사용할수는 없는 걸까??
우리의 자바, 그런 건 없으시다. 그래도 직접 만들어볼 수는 있다. 좀 복잡하게...
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;
}
}
이렇게까지 필요할까? 필요한 기능에 비해 과한 코드가 필요한듯 하다. Guava
의 ForwardingMap
을 상속해서 구현해볼 수도 있다.
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());
}
}
쓰읍... 역시 직접 사용하기엔 조금 아닌 것 같다. 그냥 key
가 null
일 때만 초기값을 넣어주거나 반환해줄 수 있으면 간단하게 구현할 수 있을 것 같다. 제네릭으로 Map
에 put
이나 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]
putIfAbsent
는 key
에 매칭되는 value
가 null
일 때 value
에 넣을 값을 지정해줄 수 있다. getOrDefault
는 key
에 매칭되는 value
가 null
일 때 지정해준 defaultValue
를 반환한다. 이 두 기능을 사용하면 자바의 Map
도 파이썬의 defaultdict
처럼 사용할 수 있다.
merge
는 key
에 매칭되는 value
가 null
이거나 아니거나 상관 없이 동작하며, 현재 value
에 1을 더한 값을 저장한다. null
일 경우 1이 저장된다.