André
André

Reputation: 328

Unexpected behavior of WeakHashMap with Map as key returning null value after modification of key

We need to cache some information about some objects therefore we are using java.util.WeakHashMap. If we the key is java.util.HashMap we are seeing unexpected behavior.

Example:

WeakHashMap<Object, Object> whm = new WeakHashMap<>();
Map<String, String> map = new HashMap<>();
whm.put(map, "map");
System.out.println(map + ": " + whm.get(map) + " " + whm + " " + whm.containsKey(map));

map.put("key", "value");
System.out.println(map + ": " + whm.get(map) + " " + whm + " " + whm.containsKey(map));
System.out.println(map.hashCode());
System.out.println(whm.entrySet().stream().map(e -> e.getKey().hashCode()).collect(Collectors.toList()));
System.out.println(whm.entrySet().stream().map(e -> e.getKey() == map).collect(Collectors.toList()));

output is:

{}: map {{}=map} true
{key=value}: null {{key=value}=map} false
112004910
[112004910]
[true]

Why is whm.get(map) null after calling whm.put(map, "map")?

Same result for java.util.HashSet...

For AtomicInteger it works as expected:

WeakHashMap<Object, Object> whm = new WeakHashMap<>();
AtomicInteger integer = new AtomicInteger(0);
whm.put(integer, "integer");
System.out.println(integer + ": " + whm.get(integer) + " " + whm + " " + whm.containsKey(integer));

integer.set(1);
System.out.println(integer + ": " + whm.get(integer) + " " + whm + " " + whm.containsKey(integer));

results in:

0: integer {0=integer} true
1: integer {1=integer} true

Upvotes: 1

Views: 335

Answers (2)

Andr&#233;
Andr&#233;

Reputation: 328

My solution was to add WeakMap using equality wich works as desired in this case:

import java.util.IdentityHashMap;
import java.util.Map;
import java.util.WeakHashMap;

/**
 * {@link WeakHashMap} also using equality to support mutable keys with changing {@link Object#hashCode()} like {@link IdentityHashMap}. In case of equality
 * checking the performance will be {@code O(n)}. <b>Currently just {@link Map#get(Object)} and {@link Map#containsKey(Object)} are supported for equality.</b>
 *
 * @author Andre Schulz
 */
public class WeakIdentityMap<K, V> extends WeakHashMap<K, V> {

    @Override
    public boolean containsKey(Object key) {
        boolean result = super.containsKey(key);

        if (result) {
            return result;
        }

        for (Map.Entry<K, V> entry : super.entrySet()) {
            if (entry.getKey() == key) {
                return true;
            }
        }

        return false;
    }

    @Override
    public V get(Object key) {
        V value = super.get(key);

        if (value != null) {
            return value;
        }

        for (Map.Entry<K, V> entry : super.entrySet()) {
            if (entry.getKey() == key) {
                return entry.getValue();
            }
        }

        return null;
    }
}

Upvotes: -1

Jon Skeet
Jon Skeet

Reputation: 1500855

This has nothing to do with it being a weak map, and everything to do with you modifying a map key, which is basically something you should avoid doing. By adding an entry to the map, you're changing its hash code. This is easily demonstrated:

import java.util.HashMap;
import java.util.Map;

public class Test {

    public static void main(String[] args) {
        Map<String, String> map = new HashMap<>();
        System.out.println(map.hashCode());
        map.put("key", "value");
        System.out.println(map.hashCode());        
    }
}

At that point trying to fetch the entry will fail, because its hash code no longer matches the hash code it was inserted with.

AtomicInteger doesn't override either equals or hashCode, so you get object identity equality instead - its hash code doesn't change when you call set.

Upvotes: 8

Related Questions