Efficient way to compare two similar maps in java

Viewed 783

Imagine I have two maps:

HashMap<TestClass, Integer> map1, map2;

with TestClass being defined as:

class TestClass {
    int i;

    public TestClass(int i){
        this.i = i;
    }
}

And the following code to fill the maps:

map1.put(new TestClass(1), 1);
map1.put(new TestClass(2), 2);

map2.put(new TestClass(1), 1);
map2.put(new TestClass(2), 2);

I want to write a method that returns true if both maps contain the "same" keys and values. Yes, in above example I could just create a local variable to store the result of the first constructor call and pass it to the second map, but I can't do this in my real application as I want to compare similar objects that do not equal each other per se at least as far as java's implementation of the equals method goes.

My previous implementation (simplified):

if(map1.size() == map2.size()){
    for(String key1 : map1.keySet()){
        if(!map1.get(key1).equals(map2.get(key1))
            return false;
    }
    return true;
} else
    return false;

This works fine for Strings, as they equal each other even if instantiated twice in different positions.

As executing the constructor of TestClass twice returns two different objects though, map2.get(key1) will return null (or throw an exception, not entirely sure) as key1 is not in map2.

To compare both maps I have written the following code (simplified):

if(map1.size() == map2.size()){
    for(TestClass key1 : map1.keySet()){
        boolean foundEqualing = false;
        for (TestClass key2 : map2.keySet()) {
            // Search equaling 
            if (key1.i == key2.i) {
                // Check if corresponding values equal as well
                if (map1.get(key1).equals(map2.get(key2))
                    // Store that a equaling key value pair was found
                    foundEqualing = true;
                // Break if keys equaled each other
                break;
            }
        }
        // Return false if no equaling key was found or if keys equal each other but the corresponding values don't
        if (!foundEqualing)
            return false;
    }
    return true;
} else
    return false;

The issue I have with this code is that it loops through both maps, which seems really inefficient to me. I'm not familiar with the correct notations, but the time the operation takes quadruples if the size of the map doubles, if I'm not mistaken.

Is there a way to more efficiently loop through or filter these maps in another way than writing for loops?

My real world code uses reflection, therefore do not focus too hard on the provided example. The types of the map could be from each and every type (the only thing I know is that they have to implement a certain interface, otherwise they are just ignored).

Edit:

I'm currently thinking about using the stream filter collect syntax, but I've never used that. Is that more efficient anyway or does it just loop over the map internally as well?

1 Answers

This could be done in a lot easier way if you could implement the equals method in your TestClass. You wouldn't have to use loops at all. You can use equals with map as well.

The way that Map.equals() works is by comparing keys and values using the Object.equals() method. This means it only works when both key and value objects implement equals() properly.

import java.util.HashMap;
import java.util.Objects;

class Main {

    public static void main(String[] args) {
        HashMap<TestClass, Integer> map1, map2;
        map1 = new HashMap<>();
        map2 = new HashMap<>();
        map1.put(new TestClass(1), 1);
        map1.put(new TestClass(2), 2);

        map2.put(new TestClass(1), 1);
        map2.put(new TestClass(2), 2);

        System.out.println(map1.equals(map2));
    }

}

class TestClass {
    int i;

    public TestClass(int i){
        this.i = i;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        TestClass testClass = (TestClass) o;
        return i == testClass.i;
    }

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

Edit: As mentioned in the comments, the above implementation cannot be used due to the use of reflection. The performance can still be improved in terms of time complexity by doing the usual trade of space and time.

Since you have to anyway write the logic to compare, the equality of two objects, I created a class with the added value field and implemented the equals logic there. I am using a HashSet for this class essentially reducing the time complexity from O(m*n) to O(max(m,n)).(Assuming sizes to be m and n).

import java.util.HashMap;
import java.util.HashSet;
import java.util.Objects;

class Main {

    public static void main(String[] args) {
        HashMap<TestClass, Integer> map1, map2;
        map1 = new HashMap<>();
        map2 = new HashMap<>();
        map1.put(new TestClass(1), 1);
        map1.put(new TestClass(2), 2);

        map2.put(new TestClass(1), 1);
        map2.put(new TestClass(2), 2);
        boolean check = checkEqual(map1, map2);
        System.out.println(check);

        //---------------------------------------

        map1 = new HashMap<>();
        map2 = new HashMap<>();
        map1.put(new TestClass(1), 1);
        map1.put(new TestClass(2), 2);

        map2.put(new TestClass(1), 1);
        map2.put(new TestClass(2), 3);
        check = checkEqual(map1, map2);
        System.out.println(check);

    }

    private static boolean checkEqual(HashMap<TestClass, Integer> map1, HashMap<TestClass, Integer> map2) {
        HashSet<TestAndValue> set = new HashSet<>();
        map1.forEach((k,v) -> set.add(new TestAndValue(k,v)));
        for(TestClass t: map2.keySet()) {
            if(!set.contains(new TestAndValue(t, map2.get(t))))
                return false;
        }
        return true;
    }


}
class TestAndValue {
    TestClass t;
    int val;

    public TestAndValue(TestClass t, int val) {
        this.t = t;
        this.val = val;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        TestAndValue that = (TestAndValue) o;
        return val == that.val && t.i == that.t.i;
    }

    @Override
    public int hashCode() {
        return Objects.hash(t.i, val);
    }
}

class TestClass {
    int i;

    public TestClass(int i){
        this.i = i;
    }
}

The Output is:

true
false

Although the implementation is messy, I hope it can give you enough idea to implement this in linear time.

Related