Samrudh S
Samrudh S

Reputation: 53

Using equals to compare map values returns false even when values and insertion order are the same

I have two Map objects: one HashMap and one LinkedHashMap. Both contain the same values (e.g., [1, 2]), and for the LinkedHashMap, the insertion order is preserved. However, when I compare the collections returned by the values() function of these maps using the equals() method, the comparison returns false.

I initially assumed this might be due to HashMap not preserving insertion order. But when I perform the same comparison with two LinkedHashMap objects, where the insertion order is indeed preserved, the equals() method still returns false.

Can someone explain why the equals() method returns false in both cases, even though:

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

public class Main {
    public static void main(String[] args) {

        // HashMap example

        Map<String, Integer> map1 = new HashMap<>();
        map1.put("a", 1);
        map1.put("b", 2);

        Map<String, Integer> map2 = new HashMap<>();
        map2.put("x", 1);
        map2.put("y", 2);

        System.out.println("map1 values: " + map1.values());  // prints [1, 2]
        System.out.println("map2 values: " + map2.values());  // prints [1, 2]

        System.out.println("map1.values().equals(map2.values()): " + map1.values().equals(map2.values())); // returns false


        // LinkedHashMap example

        Map<String, Integer> map1l = new LinkedHashMap<>();
        map1l.put("a", 1);
        map1l.put("b", 2);

        Map<String, Integer> map2l = new LinkedHashMap<>();
        map2l.put("x", 1);
        map2l.put("y", 2);

        System.out.println("map1l values: " + map1l.values()); // prints [1, 2]
        System.out.println("map2l values: " + map2l.values()); // prints [1, 2]

        System.out.println("map1l.values().equals(map2l.values()): " + map1l.values().equals(map2l.values())); // still returns false
    }
}

Upvotes: 1

Views: 143

Answers (3)

dani-vta
dani-vta

Reputation: 7130

The reason why the collections returned by the two instances of HashMap are considered not equal, even though they contain the same elements in the same order, is because the actual implementation of Collection returned by HashMap.values() is an inner class of HashMap called Values, which doesn't provide a proper definition of the equals() method.

HashMap$Values inherits the definition of its equals() method from the Object class, which only compares the memory address of the current object with the given argument. Hence, since the method compares two different objects residing in two different memory addresses, the method returns false.

The same thing also happens when you're comparing the collections returned by your LinkedHashMap instances. In this case though, you're comparing two instances of LinkedHashMap$LinkedValues that don't provide a proper definition of equals() as well, inheriting its definition from Object.

If you need to compare the values returned by a Map implementation, you could pass the collection returned by values() to the conversion constructor of:

  • ArrayList if duplicate elements are allowed and order matters.
  • HashSet if elements must be unique and order is not relevant.

Also, make sure that the values' class provides a proper definition of equals() and hasCode() according to the general contract of hashcode, since the definitions of equals() offered by ArrayList and HashSet are based on the equals() of their elements.

//Comparing the LinkedHashMap values via ArrayList
List<Integer> list1 = new ArrayList<>(map1l.values());
List<Integer> list2 = new ArrayList<>(map2l.values());
System.out.println(list1.equals(list2));   //true

//Comparing the LinkedHashMap values via HashSet
Set<Integer> set1 = new HashSet<>(map1l.values());
Set<Integer> set2 = new HashSet<>(map2l.values());
System.out.println(set1.equals(set2));   //true

Upvotes: 7

WJS
WJS

Reputation: 40047

If you do

System.out.println(map1l.values().getClass().getSimpleName());

it prints

LinkedValues

Looking at the source code, that class definition is

final class LinkedValues extends AbstractCollection<V> implements 
     SequencedCollection<V> {

Nothing in that class hierarchy is overriding equals inherited from Object. So the only thing that is being compared are the references of each collection. To properly compare them you can do this.

System.out.println(new ArrayList<>(map1l.values()).equals(new ArrayList<>(map2l.values())));

which prints true

Using an IDE like Eclipse facilitates explaining things that otherwise wouldn't make sense as you can track the implementation details of the API code. Otherwise you can examine the source code directly.

One caveate: the Objects in each collection must override equals, otherwise you are once again, just comparing references.

Upvotes: 3

VGR
VGR

Reputation: 44414

From an object-oriented standpoint: Map.values() returns a Collection, and there are absolutely no guarantees about two Collections being equal.

Subclasses of Collection (List and Set) do have such guarantees, but if you are given two objects which are Collections but which are not guaranteed to be Lists or Sets, there is no guarantee they will be considered equal, regardless of their elements.

Of course, you can easily convert them to Sets:

Set<?> values1 = Set.copyOf(map1.values());
Set<?> values2 = Set.copyOf(map2.values());
System.out.println("values equal: " + values1.equals(values2));

However, be aware that Sets cannot have duplicate values, so while this will work for your example, it will not account for Maps where the same value is present for more than one key. To handle that, you can create sorted Lists:

List<Integer> values1 = new ArrayList<>(map1.values());
List<Integer> values2 = new ArrayList<>(map2.values());
values1.sort(null);
values2.sort(null);
System.out.println("values equal: " + values1.equals(values2));

Upvotes: 7

Related Questions