[Java] LinkedHashMap의 방어적 복사
0. 들어가기전에
이번 블랙잭 미션을 하면서 결과 출력을 할 때, LinkedHashMap을 방어적 복사를 하려고 Map.copyOf()를 쓴 적이 있었다. 하지만, 이 방법은 기존 순서를 유지하지 못했다. 이 글을 통해 순서가 깨지는 이유와 방어적 복사를 하는 방법을 알아보자.
1. LinkedHashMap이란?
Hash table and linked list implementation of the Map interface, with predictable iteration order. This implementation differs from HashMap in that it maintains a doubly-linked list running through all of its entries. This linked list defines the iteration ordering, which is normally the order in which keys were inserted into the map (insertion-order). Note that insertion order is not affected if a key is re-inserted into the map. (A key k is reinserted into a map m if m.put(k, v) is invoked when m.containsKey(k) would return true immediately prior to the invocation.)
LinkedHashMap의 정의에 따르면, LinkedHashMap은 HashMap과 달리 각각의 Entry이 이중 연결 리스트로 구현되어 있다고 한다. 그렇기 때문에 LinkedHashMap은 key의 순서를 보장할 수 있다.
2. Map.copyOf()
@Test
void linkedHashMap() {
final Map<Integer, Integer> linkedHashMap = new LinkedHashMap<>();
for (int i = 0; i < 10; i++) {
linkedHashMap.put(i, i);
}
final Map<Integer, Integer> copyLinkedHashMap = Map.copyOf(linkedHashMap);
for (Integer key : copyLinkedHashMap.keySet()) {
System.out.println(key);
}
}
위 코드는 0 ~ 9까지 LinkedHashMap에 넣고 copyOf()로 복사한 상황에서 키 값을 순서대로 출력하는 로직을 나타낸다. 결과는 어떻게 나올까?
5
6
7
8
9
0
1
2
3
4
놀랍게도 순서가 유지가 되지 않는 채 key 값이 출력되는 것을 확인 할 수 있다. 자세히 확인하기 위해서 Map.copyOf()의 내부 동작을 살펴보자.
static <K, V> Map<K, V> copyOf(Map<? extends K, ? extends V> map) {
if (map instanceof ImmutableCollections.AbstractImmutableMap) {
return (Map<K,V>)map;
} else {
return (Map<K,V>)Map.ofEntries(map.entrySet().toArray(new Entry[0]));
}
}
@SafeVarargs
@SuppressWarnings("varargs")
static <K, V> Map<K, V> ofEntries(Entry<? extends K, ? extends V>... entries) {
if (entries.length == 0) { // implicit null check of entries array
return ImmutableCollections.emptyMap();
} else if (entries.length == 1) {
// implicit null check of the array slot
return new ImmutableCollections.Map1<>(entries[0].getKey(),
entries[0].getValue());
} else {
Object[] kva = new Object[entries.length << 1];
int a = 0;
for (Entry<? extends K, ? extends V> entry : entries) {
// implicit null checks of each array slot
kva[a++] = entry.getKey();
kva[a++] = entry.getValue();
}
return new ImmutableCollections.MapN<>(kva);
}
}
Map의 copyOf 메서드를 보면 이미 ImmutableCollection의 구현체이면 그 값을 반환하고, 그렇지 않으면 Map.ofEntries()를 사용하여 바꿔 준다. ofEntries에서 보면 모두 ImmputableCollections를 반환하고 있지만, 어디에도 LinkedHashMap과 관련된 코드를 찾을 수가 없다.
※ ImmutableCollections
private ImmutableCollections() { }
/**
* The reciprocal of load factor. Given a number of elements
* to store, multiply by this factor to get the table size.
*/
static final int EXPAND_FACTOR = 2;
static UnsupportedOperationException uoe() { return new UnsupportedOperationException(); }
static abstract class AbstractImmutableCollection<E> extends AbstractCollection<E> {
// all mutating methods throw UnsupportedOperationException
@Override public boolean add(E e) { throw uoe(); }
@Override public boolean addAll(Collection<? extends E> c) { throw uoe(); }
@Override public void clear() { throw uoe(); }
@Override public boolean remove(Object o) { throw uoe(); }
@Override public boolean removeAll(Collection<?> c) { throw uoe(); }
@Override public boolean removeIf(Predicate<? super E> filter) { throw uoe(); }
@Override public boolean retainAll(Collection<?> c) { throw uoe(); }
}
ImmutableCollections는 어떠한 마법을 사용해서 Collection들을 변경을 못하도록 하는것이 아니라 Override를 통해서 변경과 관련된 메서드를 금지하고 있다.(다형성)
3. LinkedHashMap의 방어적 복사
그렇다면 LinkedHashMap은 방어적 복사를 어떻게 해야할까?
3.1 새로운 LinkedHashMap을 통한 복사
private static Map<Integer, Integer> defensiveCopy(final Map<Integer, Integer> linkedHashMap) {
final Map<Integer, Integer> newLinkedHashMap = new LinkedHashMap<>();
for (Integer key : linkedHashMap.keySet()) {
newLinkedHashMap.put(key, linkedHashMap.get(key));
}
return Collections.unmodifiableMap(newLinkedHashMap);
}
- 모두가 알고 있는 방법!
3.2 stream()을 통한 복사
private static Map<Integer, Integer> defensiveCopy(final Map<Integer, Integer> linkedHashMap) {
return linkedHashMap.keySet().stream()
.collect(toMap(
Function.identity(),
linkedHashMap::get,
(oldInteger, newInteger) -> newInteger,
() -> Collections.unmodifiableMap(new LinkedHashMap<>()))
);
}
- 개인적으로 이 방법은 가독성이 정말 안 좋은것 같다.(스트림에 주화입마가 오면 생각나는 방법)
3.3 Collections.unmodifiableMap + new LinkedHashMap()
private static Map<Integer, Integer> defensiveCopy(final Map<Integer, Integer> linkedHashMap) {
return Collections.unmodifiableMap(new LinkedHashMap<>(linkedHashMap));
}
- 이 방법이 가장 깔끔하고 좋다.
'LANGUAGE > [JAVA]' 카테고리의 다른 글
[Java] EnumMap이란? (1) | 2023.03.24 |
---|---|
[Java] JVM - Garbage Collector (2) | 2023.03.12 |
[Java] JVM - 메모리 구조 (4) | 2023.02.28 |
[Java] 롬복의 원리 (0) | 2022.06.08 |
[JAVA] JVM (0) | 2022.06.04 |