본문 바로가기
Study/Java

어질어질 LinkedHashMap 복사하기

by dev_kong 2023. 3. 13.
728x90
728x90

레벨1 - 블랙잭 미션을 진행하며, LinkedHashMap을 이용하여 플레이어와 플레이어의 배팅 금액을 저장하였었다.

private final Map<Player, BettingAmount> bettingMap = new LinkedHashMap<>();

LinkedHashMap을 사용한 이유는 플레이어가 생성된 순서대로 베팅금액을 입력받고,
그 순서대로 출력되기를 원했기 때문이다.

 

출력을 위해 당당하게 getter를 사용했는데, Map을 그냥 던져주기엔 불안하다.
Map.copyOf()를 통해 복사된 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]));  
    }
}

copyOf method를 까보면, 파라미터로 전달된 객체가 immutableMap이면 파라미터를 그대로 리턴하고,
아닌 경우 Map.ofEntries()를 통해 새로운 Map을 만드는 것을 확인할 수 있다.

 

ㅎㅎ.. ofEntries() 도 까보자..

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);  
    }
}

뭐 잘은 모르겠지만, 빈맵인 경우 emptyMap을 반환하고, entry가 1개면 Map1을 반환한다.

그리고 나머지의 경우는 MapN을 반환하는 것을 볼 수 있다.

 

MapN 역시 까보긴 했지만 무슨 말인지 모르겠다.


다만, 확신 할 수 있는 건 copyOf의 파라미터로 HashMap이 들어오건, LinkedHashMap이 들어오건 TreeMap 이 들어오건 상관없이 MapN으로 만들어서 반환한다.

 

즉, LinkedHashMap이 들어왔다고 해서, 순서를 유지해주는 작업따위는 해주지 않는다.

Collections.unmodifiableMap()

순서 유지를 위해서 Collections.unmodifiableMap()을 사용했다.

@Test  
@DisplayName("unmodifiableMap 순서 확인 테스트")  
void unmodifiableMap() {
    Map<String, String> originalMap = new LinkedHashMap<>();
    for (int i = 0; i < 100; i++) {  
        String key = "key" + i;  
        String value = "value" + i;  
        originalMap.put(key, value);  
    }

    Map<String, String> copyMap = Collections.unmodifiableMap(originalMap);  

    assertThat(copyMap.keySet()).containsExactlyElementsOf(originalMap.keySet());  
}

Collections.unmodifiableMap()을 사용하면 순서를 유지할 수 있기 때문에 위 테스트 코드를 돌려보면 통과하는걸 확인할 수 있다.


그런데 Collections.unmodifiableMap() 은 어떻게 순서를 유지하는 걸까?
역시나 한번 까보자.

public static <K,V> Map<K,V> unmodifiableMap(Map<? extends K, ? extends V> m) {  
    return new UnmodifiableMap<>(m);  
}  

private static class UnmodifiableMap<K,V> implements Map<K,V>, Serializable {  
    ...

    private final Map<? extends K, ? extends V> m;  

    UnmodifiableMap(Map<? extends K, ? extends V> m) {  
        if (m==null)  
            throw new NullPointerException();  
        this.m = m;  
    }  
    public int size()                        {return m.size();}  
    public boolean isEmpty()                 {return m.isEmpty();}  
    public boolean containsKey(Object key)   {return m.containsKey(key);}  
    public boolean containsValue(Object val) {return m.containsValue(val);}  
    public V get(Object key)                 {return m.get(key);}  

    public V put(K key, V value) {  
        throw new UnsupportedOperationException();  
    }    public V remove(Object key) {  
        throw new UnsupportedOperationException();  
    }    public void putAll(Map<? extends K, ? extends V> m) {  
        throw new UnsupportedOperationException();  
    }    public void clear() {  
        throw new UnsupportedOperationException();  
    }
}

Collections.unmodifiableMap() 은 Map안의 요소를 가져와서 Map을 재구성 하는 것이 아니라,
인자로 받는 Map의 참조를 필드로 저장하고, Map을 수정하는 메서드들을 호출하지 못하도록 예외를 던져준다.

 

즉, 새로운 Map을 만드는 것이 아니라 원래의 참조를 그대로 유지하면서 수정만 막아둔 것이다.


그렇다면, 참조가 끊겨있지 않기 때문에 원본의 값이 변경되면 복사본의 값 역시 변경될것이다.

아까의 테스트를 다시 가져와 보자.

@Test  
@DisplayName("unmodifiableMap 순서 확인 테스트")  
void unmodifiableMap() {
    Map<String, String> originalMap = new LinkedHashMap<>();
    for (int i = 0; i < 100; i++) {  
        String key = "key" + i;  
        String value = "value" + i;  
        originalMap.put(key, value);  
    }

    Map<String, String> copyMap = Collections.unmodifiableMap(originalMap);  

    // 원본에 새로운 값 추가
    originalMap.put("newKey", "newValue");

    assertThat(copyMap.keySet()).containsExactlyElementsOf(originalMap.keySet());  
}

copyMap이 originalMap의 참조를 유지 하고 있으니 여전히 테스트가 통과한다.


Collections.unmodifiableMap() 은 말그대로 unmodifiable 하지만, immutable 하진 않다.

 

그럼 잠시 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]));  
    }
}

copyOf 같은 경우는 파라미터로 전달된 값이 immutable이 아니면, 새로운 map을 만들어서 참조를 끊어 주지만,
이미 immutable 한 값이 파라미터로 전달 되면 참조를 끊지 않고 원본map을 그대로 리턴한다.

 

참조를 끊는 이유는 원본이 변하면 복사본의 값역시 변하는 것을 방지 하기 위함인데
원본이 immutable이라, 원본이 변할리가 없으니 그냥 그대로의 값을 리턴하는 것이다.

굉장히 합리적이다...

결론

copyOf는 원본과의 참조가 끊기기 때문에, 원본의 변경으로 부터 자유롭다. 단, LinkedHashMap의 순서는 유지해주지 않는다.
unmodifiableMap는 LinkedListMap의 순서는 유지해주지만, 원본과의 참조가 유지되고 있기에 원본의 변경으로 부터 자유롭지 못하다.

 

원본과의 참조도 끊고, 순서도 유지하고 싶고, unmodifiable하게 만들고 싶다면 다음과 같이 해주면 된다.

Map<String, String> copyMap = Collections.unmodifiableMap(new LinkedHashMap<>(originalMap));

새로운 LinkedHashMap 을 생성하여 원본과의 참조를 끊어내고,
unModifiableMap을 이용하여 변경을 방지해주면 된다.

하핳 참.. 복사하기 힘들다.

이번에 사용한게 Map이라 Map을 들어 설명 했는데, 모든 Collection에 적용되는 내용이다.
해당 collection의 성질? 특징? 같은걸 유지하면서 복사하고 싶다면 copyOf는 안쓰는 게 좋을 듯.

728x90
728x90

댓글