简体   繁体   中英

Getting the current key of a map entry given another equivalent key object

Suppose I have a HashMap<K, V> and two objects of type K that are equal to each other but not the same object, and the map has an entry for key k1 .

Given k2 , can I get a reference to k1 using only methods from HashMap (no external data structures) that executes in constant time, ie O(1) time complexity?

In code:

K k1, k2;
k1.equals(k2) // true
k1.hashCode() == k2.hashCode() // true
k1 == k2 // false
myMap.put(k1, someValue);

K existingKey = getExistingKey(myMap, k2);
existingKey == k1 // true <- this is the goal

<K> K getExistingKey(HashMap<K, V> map, K k) {
    // What impl goes here?
}

I was hoping to use one of the various methods added with java 8, such as compute() to "sniff" the existing key within the lambda, but they all (seem to) pass the new key object to the lambda, not the existing key.

Iterating through the entrySet() would find the existing key, but not in constant time.

I could use a Map<K, K> to store the keys and I could keep it in sync, but that doesn't answer the question.

You are looking for something like

Map.Entry<K,V> getEntry(K key)

At first I thought it would be easy to make a custom subclass of HashMap to return this, as get(K key) is just

public V get(Object key) {
     Node<K,V> e;
     return (e = getNode(hash(key), key)) == null ? null : e.value;
}

where Node implements Map.Entry . This would look like:

public class MyHashMap<K,V> extends HashMap<K,V>
{
    public MyHashMap() {}
    // Other constructors as needed

    public Map.Entry<K, V> getEntry(K key)
    {
        Map.Entry<K, V> e = getNode(hash(key),key);
        return e;
    }
}

Unfortunately, getNode() and hash() are both package private and therefore not visible to subclasses.

The next step was to put the class in java.util but this fails in Java 9 with

The package java.util conflicts with a package accessible from another module: java.base

I think you're out of luck here.

I actually think a getEntry() method would be a useful addition to the API, you might consider filing an enhancement request.

I don't know how constrained you are regarding memory usage, but if you can use a LinkedHashMap instead of a HashMap ( LinkedHashMap uses extra references to keep insertion order), then you could take advantage of its removeEldestEntry method:

public class HackedMap<K, V> extends LinkedHashMap<K, V> {

    K lastKey;

    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        lastKey = eldest.getKey();
        return false;
    }

    K getLastKey() {
        return lastKey;
    }
}

I think the code is self-explanatory. We are keeping a reference to the original key, which we grab from the removeEldestEntry method's argument. As to the removeEldestEntry method's return value, it is false , so that we don't allow the eldest entry to be removed (after all, we don't want the map to work as a cache).

Now, with a common insertion-order LinkedHashMap , the removeEldestEntry method is automatically called by put and putAll (from removeEldestEntry method docs):

This method is invoked by put and putAll after inserting a new entry into the map.

So all we need to do now is to implement your getExistingKey method in such a way that it calls put without modifying the map, which you can do as follows:

<K, V> K getExistingKey(HackedMap<K, V> map, K k) {
    if (k == null) return null;
    V v = map.get(k);
    if (v == null) return null;
    map.put(k, v);
    return map.getLastKey();
}

This works because, when the map already contains an entry mapped to a given key, the put method replaces the value without touching the key.

I'm not sure about the null checks I've done, maybe you need to improve that. And of course this HackedMap doesn't support concurrent access, but HashMap and LinkedHashMap don't do either.

You can safely use a HackedMap instead of a HashMap . This is the test code:

Key k1 = new Key(10, "KEY 1");
Key k2 = new Key(10, "KEY 2");
Key k3 = new Key(10, "KEY 3");

HackedMap<Key, String> myMap = new HackedMap<>();

System.out.println(k1.equals(k2)); // true
System.out.println(k1.equals(k3)); // true
System.out.println(k2.equals(k3)); // true

System.out.println(k1.hashCode() == k2.hashCode()); // true
System.out.println(k1.hashCode() == k3.hashCode()); // true
System.out.println(k2.hashCode() == k3.hashCode()); // true

System.out.println(k1 == k2); // false
System.out.println(k1 == k3); // false
System.out.println(k2 == k3); // false

myMap.put(k1, "k1 value");
System.out.println(myMap); // {Key{k=10, d='KEY 1'}=k1 value}

myMap.put(k3, "k3 value"); // Key k1 (the one with its field d='KEY 1') remains in
                           // the map but value is now 'k3 value' instead of 'k1 value'

System.out.println(myMap); // {Key{k=10, d='KEY 1'}=k3 value}

Key existingKey = getExistingKey(myMap, k2);

System.out.println(existingKey == k1); // true
System.out.println(existingKey == k2); // false
System.out.println(existingKey == k3); // false

// Just to be sure
System.out.println(myMap); // {Key{k=10, d='KEY 1'}=k3 value}

Here's the Key class I've used:

public class Key {

    private final int k;

    private final String d;

    Key(int k, String d) {
        this.k = k;
        this.d = d;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Key key1 = (Key) o;
        return k == key1.k;
    }

    @Override
    public int hashCode() {
        return Objects.hash(k);
    }

    @Override
    public String toString() {
        return "Key{" +
                "k=" + k +
                ", d='" + d + '\'' +
                '}';
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM