Home > Enterprise >  HashMap with List of Objects as a Key
HashMap with List of Objects as a Key

Time:04-13

in HashMap when I pass List of Objects as Key I get different results.

List<NewClass> list1 = new ArrayList<>();
List<NewClass> list2 = new ArrayList<>();

NewClass obj1 = new NewClass(1, "ddd", "[email protected]");
NewClass obj2 = new NewClass(2, "ccc", "[email protected]");

list1.add(obj1);
list1.add(obj2);

list2.add(obj1);
list2.add(obj2);

Map<List<NewClass>, Integer> mapClass = new HashMap<>();
mapClass.put(list1, 1234);
mapClass.put(list2, 4567);

System.out.println(mapClass.size());
System.out.println(mapClass.get(list1));

NewClass obj4 = new NewClass(1, "ddd", "[email protected]");
NewClass obj5 = new NewClass(2, "ccc", "[email protected]");
List<NewClass> list3 = new ArrayList<>();
list3.add(obj4);
list3.add(obj5);

System.out.println(mapClass.get(list3));

System.out.println(list1.hashCode());
System.out.println(list2.hashCode());
System.out.println(list3.hashCode());

Below is the output I see

hashCode() called - Computed hash: -1704251796
hashCode() called - Computed hash: -587009612
hashCode() called - Computed hash: -1704251796
hashCode() called - Computed hash: -587009612
1
hashCode() called - Computed hash: -1704251796
hashCode() called - Computed hash: -587009612
4567
hashCode() called - Computed hash: -1704251796
hashCode() called - Computed hash: -587009612
**null**
hashCode() called - Computed hash: -1704251796
hashCode() called - Computed hash: -587009612
-1879206775
hashCode() called - Computed hash: -1704251796
hashCode() called - Computed hash: -587009612
-1879206775
hashCode() called - Computed hash: -1704251796
hashCode() called - Computed hash: -587009612
-1879206775

Even though hashcode is same for all the 3 lists, mapClass.get(list3) is retuning null. list3 has same object as list1 / list2. Why is this behaviour ?

CodePudding user response:

From map V get(Object key) documentation:

 * ... if this map contains a mapping from a key
 * {@code k} to a value {@code v} such that
 * {@code Objects.equals(key, k)},
 * then this method returns {@code v}; otherwise
 * it returns {@code null}. ...

I'm not sure how you implemented the equals method of NewClass, but the following implementation of NewClass doesn't return null when calling System.out.println(mapClass.get(list3));

public class NewClass {
    private int id;
    private String name;
    private String mail;

    NewClass(int id,String name,String mail){
        this.id = id;
        this.name = name;
        this.mail = mail;
    }

    @Override
    public int hashCode() {
        return id * name.hashCode() * mail.hashCode();
    }

    @Override
    public boolean equals(Object o) {

        if (o == this) return true;
        if (!(o instanceof NewClass)) {
            return false;
        }
        NewClass newClass = (NewClass) o;

        return newClass.id == id &&
               newClass.name.equals(name) &&
               newClass.mail.equals(mail);
    }
}

Also, as mentioned in the comments mutable keys are not a good idea, please check enter image description here

ArrayList's hashCode()

enter image description here

enter image description here

HashMap Implementation

The HashMap is implemented as a Hash table containing an array of buckets. Each key's hashCode() is mapped to a bucket array index and different keys are allowed to have the same hashcode ([hashCode contract][4]). That's why each bucket can contain multiple key/value pair arranged via a Linked data structure. So, when you're invoking the get method, first a bucket is selected by mapping the key's hashCode to the bucket index, then the target entry is searched by calling the equals() method on the key.


Code Explanation

As shown from the output, after adding the second pair with a different list as the key, we can see that the map's size is still 1. This is because you've used the exact same references (obj1 and obj2) to build both the first and second key, yielding the same hashCode for list1 and list2 as the ArrayList's hashCode is built upon its elements. Once, the second pair is added to the HashMap, its key's hashCode returns the same value of the first key, indexing the same bucket and then replacing the first pair once the second key equals the key of the first pair.

Now getting to your question. The situation described would have taken place even if the List's elements had been different references with same values as long the NewClass' equals() method had been defined on the exact three fields passed to the constructor (int, String, String). My guess is that the NewClass' equals() method hasn't been defined or it confronts different fields. Both equals() and hashCode should work on the same set of fields. In fact, if we define your NewClass as follows, also the third addition will replace the only pair contained within the HashMap.

public class Test {
    public static void main(String[] args) {
        List<NewClass> list1 = new ArrayList<>();
        List<NewClass> list2 = new ArrayList<>();

        NewClass obj1 = new NewClass(1, "ddd", "[email protected]");
        NewClass obj2 = new NewClass(2, "ccc", "[email protected]");

        list1.add(obj1);
        list1.add(obj2);

        list2.add(obj1);
        list2.add(obj2);

        Map<List<NewClass>, Integer> mapClass = new HashMap<>();
        mapClass.put(list1, 1234);
        mapClass.put(list2, 4567);

        System.out.println(mapClass.size());
        System.out.println(mapClass.get(list1));

        NewClass obj4 = new NewClass(1, "ddd", "[email protected]");
        NewClass obj5 = new NewClass(2, "ccc", "[email protected]");
        List<NewClass> list3 = new ArrayList<>();
        list3.add(obj4);
        list3.add(obj5);

        System.out.println(mapClass.get(list3));

        System.out.println(list1.hashCode());
        System.out.println(list2.hashCode());
        System.out.println(list3.hashCode());
    }
}

class NewClass {
    int id;
    String s1, s2;

    public NewClass(int id, String s1, String s2) {
        this.id = id;
        this.s1 = s1;
        this.s2 = s2;
    }

    public int hashCode() {
        return Objects.hash(id, s1, s2);
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == this) return true;
        if (obj == null || obj.getClass() != getClass()) return false;
        NewClass nc = (NewClass) obj;
        return nc.id == id && Objects.equals(s1, nc.s1) && Objects.equals(s2, nc.s2);
    }
}


Output

enter image description here

Conclusions

In conclusion, as others have already said, you shouldn't be using mutable objects as keys for a HashMap. The changes applied on a key internal state may alter its hashCode, making its paired value untraceable or even worst retrieving another key's value in a very remote scenario. These are some guidelines on how to design a HashMap key that might be helpful:
  • Related