javacollectionshashmapequalshashcode

HashMap with List of Objects as a Key


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", "eee@gmail.com");
NewClass obj2 = new NewClass(2, "ccc", "kkk@gmail.com");

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", "eee@gmail.com");
NewClass obj5 = new NewClass(2, "ccc", "kkk@gmail.com");
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 ?


Solution

  • Code Problems

    The problem with your code lies in many aspects:

    As others have already pointed out, you're using ArrayList instances as keys, which is a bad design choice, since the hashcode of mutable objects is subject to change, introducing the risk of losing access to the paired values.

    Furthermore, since the implementation of ArrayList.equals() and ArrayList.hashCode() is based on the equals() and hashCode() of its elements, you also need to make sure that their class implements the methods properly.

    HashMap Implementation

    The HashMap class is implemented as an array of buckets, where a bucket is either a linked list or a tree structure. When a key-value pair is added to a HashMap, the key's hashcode is mapped to a bucket's index, and then the entry is added to the bucket. This means that a bucket can contain multiple entries with different keys, since different objects can share the same hashcode (see the hashCode contract). Once a bucket has been selected, the HashMap verifies whether this bucket contains a pair whose key is equal to the key of the new entry. If it does, the pair with the matching key is updated with the new entry's value; if it doesn't, the new entry is simply added to the bucket.

    Code Explanation & Answer

    As shown from the output above, after adding the second entry with a different list as its 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 construct both the first and second key. In this scenario, regardless of NewClass providing a definition for equals() and hashCode(), the two lists will always be equal and yield the same hashcode. This is because the hashcode of an ArrayList is computed from the hashcode of its elements (both lists contain the same references). While ArrayList.equals() relies on the Objects.equals method, which first compares the memory address of the two given references, and then invokes the equals() method of the first parameter. Hence, the second entry updating the first one, as explained in the section above.

    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?

    Instead, when you're trying to retrieve with list3 the value associated to list1 or list2, you're getting null because NewClass does not provide a proper definition of the equals() and hashCode() methods. These methods should be defined on the same set of attributes as explained in the equals and hashCode contract.

    Here, I've attached an implementation of NewClass which honors the equals() and hashCode() contract, and that allows the first entry's value to be returned when list3 is passed.

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

    Conclusions

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