Google Guava Maps difference

Introduction

There would have been scenarios where you would want to find all the differences between two map instances. In this post, we will explore what finding difference between two maps means and how using the Google Guava’s Maps#difference method we can do this.

Finding difference between two maps

Let us say we have two maps, as shown below.

Map<String, Integer> left = Map.of(
        "a", 1,
        "b", 2,
        "c", 3);
Map<String, Integer> right = Map.of(
        "b", 2,
        "c", 33,
        "d", 4);

Note: The Map.of static method was added in Java 9. See the Convenience Factory Methods for Collections post to learn more about it.

Here, we want to find all the differences between the two maps (left and right). We can classify the differences into four categories.

  1. The map entries present only in the left map (whose keys aren’t present in the right map)
  2. The map entries present only in the right map (whose keys aren’t present in the left map)
  3. The map entries present in both the maps, i.e., each of the entries’ keys and the values are the same between the two maps. This is the intersection of the two maps.
  4. The entries differing between the two maps. These are entries with keys in both the maps, but with different values.

In our example,

  1. The entry {a=1} is present only in the left map.
  2. The entry {d=4} is present only in the right map.
  3. The entry {b=2} is present in both the maps.
  4. The entry with key c differs between both the maps. In the left map, the value is 3, while the right map has value mapped to key c as 33.

There is no need to write a utility from scratch to find and return all these categories of differences. We can make use of Google Guava’s MapDifference for this.

We can get all the above explained categories of differences between two maps by calling the static utility method called difference() on the Maps utility class. Let us now explore the Maps#difference method in Google Guava.

Google Guava MapDifference interface

When we call the Maps#difference utility method, it returns an instance of MapDifference. But before we dive into the Maps#difference method, let us first look at the MapDifference interface.

The MapDifference interface is declared as,

interface MapDifference<K extends @Nullable Object,V extends @Nullable Object>

It is a generic interface with two type parameters, K and V, which correspond to the types of the keys and values in the map, respectively. In short, it represents the differences between two maps.

MapDifference interface methods

The MapDifference interface has the following methods. These correspond to the various categories of differences we saw earlier.

  1. areEqual(): This will return true if there are no differences between the two maps.
  2. entriesOnlyOnLeft(): This will return a Map<K, V> which has all the entries from the left map whose keys aren’t present in the right map. The returned map is unmodifiable.
  3. entriesOnlyOnRight(): This will return a Map<K, V> which has all the entries from the right map whose keys aren’t present in the left map. Again, the returned map is unmodifiable.
  4. entriesInCommon(): This returns an unmodifiable map (Map<K, V) which contains the entries which are present in both the maps.
  5. entriesDiffering(): This returns an unmodifiable map containing keys which are present in both maps, but with difference values. Calling this, it returns a Map<K, ValueDifference<V>>.

The ValueDifference is an interface (a package-private interface) as shown below. It has two methods to get the left and the right value.

interface ValueDifference<V extends @Nullable Object> {
    V leftValue();

    V rightValue();

    //equals and hashCode omitted
}

Maps#difference method in Google Guava

Let us now look at three overloaded Maps#difference method in the Google Guava utility class Maps.

The first version of the difference method we will see takes two Map instances (left and right) and computes the difference between them. It returns the difference as a MapDifference<K, V>. This difference is an immutable snapshot and hence it will not be updated even if the map contents are updated later on.

The method signature is shown below.

public static <K extends @Nullable Object, V extends @Nullable Object>
      MapDifference<K, V> difference(
          Map<? extends K, ? extends V> left, 
          Map<? extends K, ? extends V> right) { ... }

Finding difference between two maps using Maps#difference

Let us use the same example used earlier and find the difference between two maps using the Google Guava Maps#difference method.

Map<String, Integer> left = Map.of(
        "a", 1,
        "b", 2,
        "c", 3);
Map<String, Integer>right = Map.of(
        "b", 2,
        "c", 33,
        "d", 4);
MapDifference<String, Integer> diff = Maps.difference(left, right);


System.out.println(diff.areEqual()); //false
System.out.println(diff.entriesOnlyOnLeft()); //{a=1}
System.out.println(diff.entriesOnlyOnRight()); //{d=4}
System.out.println(diff.entriesInCommon()); //{b=2}
System.out.println(diff.entriesDiffering()); //{c=(3, 33)}
  • Calling the areEqual() method returns false as the two maps are not “equal”.
  • The entry {a=1} is the only entry present in the left map.
  • The entry {d=4} is the only entry present in the right map.
  • There is only one common entry (same key and same value) ({b=2}).
  • Finally, there is one differing entry corresponding to the key c. In the left map, the value is 3, and in the right map, the value is 33.

The toString implementation of the ValueDifference interface returns a string by joining the left and the right values with a comma (,).

public String toString() {
      return "(" + left + ", " + right + ")";
 }

The MapDifference implementation also has a useful toString representation, which includes all the categories of the differences.

Printing the MapDifference instance as below we get,

System.out.println(diff);
not equal: only on left={a=1}: only on right={d=4}: value differences={c=(3, 33)}

It starts the toString representation, by stating if the maps are equal or not. Then, it includes all the categories of the differences viz.,

  1. Entries present only on the left map.
  2. Entries present only on the right map.
  3. The differing entries.

It doesn’t include the common entries in the toString output.

It wouldn’t show a category if it is empty. For example, shown below, we have two maps with only differing entries. In this case, the toString of the MapDifference doesn’t include only on left and only on right categories.

Map<String, Integer> left = Map.of("a", 1);
Map<String, Integer> right = Map.of("a", 11);
System.out.println(Maps.difference(left, right)); //not equal: value differences={a=(1, 11)}

Difference between two equal maps using Maps#difference

Let us look at an example when using the difference method with two equal maps.

Map<String, Integer> left = Map.of(
        "a", 1,
        "b", 2);
Map<String, Integer> right = Map.of(
        "a", 1,
        "b", 2);
diff = Maps.difference(left, right);

System.out.println(diff.areEqual()); //true
System.out.println(diff.entriesOnlyOnLeft()); // {}
System.out.println(diff.entriesOnlyOnRight()); //{}
System.out.println(diff.entriesInCommon()); //{a=1, b=2}    
System.out.println(diff.entriesDiffering()); //{}

System.out.println(diff); //equal
  • Calling areEqual() returns true as the maps are equal.
  • Since the two maps are equal, the methods entriesOnlyOnLeft and entriesOnlyOnRight returns empty maps.
  • The entriesInCommon method returns the entire map content ({a=1, b=2})
  • There are no entries which differ between the two maps.

The toString of the MapDifference prints the string “equal” to indicate that the two maps are equal. Since there are no other categories of differences, nothing else is included in the toString result.

Google Guava Equivalence

The second overloaded Maps#difference method allows us to pass an instance of Equivalence. Thus, let us take a small detour and learn about Equivalence.

An Equivalence is a strategy or a way to determine whether two instances are considered equivalent. It supports two strategies:

  • The identity equivalence
  • The equals equivalence

Identity and Equals equivalence

The Equivalence abstract class has two static methods viz., identity() and equals() which return an instance of Equivalence that does the equivalence check based on identity and equality, respectively.

The identity-based equivalence uses instance identity to compare values (is uses ==). In other words, two values or instances are equivalent if and only if they refer to the same instance (reference based equivalence). It uses System.identityHashCode to compute the hash code for an instance.

On the other hand, equivalence based on equals uses the object instance’s equals() and hashCode() to check for equivalence.

Equivalence<Object> identityEquivalence = Equivalence.identity();
String a = "abc";
String b = "abc";
System.out.println(identityEquivalence.equivalent(a, b)); //true
System.out.println(identityEquivalence.equivalent(a, a)); //true
System.out.println(identityEquivalence.equivalent(a, new String("abc"))); //false

In the above code, we have an instance of equivalence which works based on identity.

The Equivalence type has a method called equivalent, which takes two objects and returns true if the passed objects are considered equivalent.

We have two strings with the same value (”abc”),

  • In the first call, we pass references to the two strings and we get true as the result. This is because, in Java, two string constants with the same value will not create two string values on the heap.
  • In the second call, we pass the same reference, and it returns true.
  • Finally, in the last call, we pass the string reference ‘a’ and a new string with the same value (”abc”). Since these are not identical (the references are different), it returns false as the result.

Let us do the same using object equivalence. We get an instance of equivalence which checks for equivalence using Object#equals using the Equivalence#equals method as shown below.

Since the first three calls passes two strings with the same value, they return back true as the result. The last call passes a string with a different value and the equivalence check returns a false back.

Equivalence<Object> objectEquivalence = Equivalence.equals();
System.out.println(objectEquivalence.equivalent(a, b)); //true
System.out.println(objectEquivalence.equivalent(a, a)); //true
System.out.println(objectEquivalence.equivalent(a, new String("abc"))); //true
System.out.println(objectEquivalence.equivalent(a, new String("def"))); //false

Let us now look at the other methods in the Equivalence class.

Equivalence#equivalentTo

The Equivalence#equivalentTo method takes a target value and returns a Predicate (Google Guava Predicate) that evaluates to true if and only if the input is equivalent to passed target value according to this equivalence relation.

In the below example, we pass a target value as the string “Java” by calling the equivalentTo method on the identityEquivalence instance. The returned Predicate will check if the passed input (to the Predicate) is equivalent to the target value (”Java”) as per the equivalence (identity equivalence). Hence, passing string literal “Java” to the predicate returns true, whereas creating a new string instance with the same value fails the (identity) equivalence check.

Predicate<Object> predicate = identityEquivalence.equivalentTo("Java");
System.out.println(predicate.test("Java")); //true
System.out.println(predicate.test(new String("Java"))); //false        

If we use an object equivalence in the same example, we will get true for both the calls and the predicate check will return false only when we pass a different string.

Predicate<Object> predicate = objectEquivalence.equivalentTo("Java"); 
System.out.println(predicate.test("Java")); //true
System.out.println(predicate.test(new String("Java"))); //true
System.out.println(predicate.test("C++")); //false

Equivalence#onResultOf

The onResultOf takes a Function to convert from type F to type T and returns a new equivalence relation for the type F (Equivalence<F>). Remember, the formal type parameter of the Equivalence class is T. Hence, this method is used to convert a value of type F to type T and then evaluates equivalence. The result is that we have an Equivalence working on type F.

The method signature is shown below.

public final <F> Equivalence<F> onResultOf(
        Function<? super F, ? extends @Nullable T> function) { ... }

Returns a new equivalence relation for F, which evaluates equivalence by first applying function to the argument, then evaluating using the (this) equivalence.

For a pair of non-null objects x and y, equivalence.onResultOf(function).equivalent(a, b) is true if and only if equivalence.equivalent(function.apply(a), function.apply(b))is true.

Let us look at a couple of examples.

In the below code, we have a function to convert a string to an integer which represents the length of the string.

Next, we start from Equivalence.equals() (which returns a Equivalence<Object>) and call the onResultOf on it passing the function. This gives us a Equivalence<String>.

Function<String, Integer> stringToItsLengthFunction = String::length;
Equivalence<String> stringEquivalenceByLength = Equivalence.equals()
        .onResultOf(stringToItsLengthFunction); //FunctionalEquivalence

Now, when we call the equivalent method on the stringEquivalenceByLength instance passing two strings, it will use the passed function to convert both the strings to Integers and use the underlying Equivalence instance to check for equivalence (which is Equivalence.equals() here).

In the below example, we pass two different strings of the same length. Since the function converts the string to an Integer and the equivalence check is based on that result, it gives back true as the result (3.equals(3) is true).

As the second call shows, using Equivalence.equals() on the two strings we used will obviously return a false.

System.out.println(stringEquivalenceByLength.equivalent("abc", "def")); //true
System.out.println(Equivalence.equals()
        .equivalent("abc", "def")); //false

Let us continue the chain and create another transformation. Below, we have a new function to convert an Integer to a String. Then we call the onResultOf on the earlier created Equivalence instance (stringEquivalenceByLength) to get an Equivalence<Integer>.

What happens when we pass two Integers to the equivalent method is as follows:

  1. First, it uses the integerToString function to convert both the Integer arguments to Strings.
  2. Next, it checks equivalence using stringEquivalenceByLength instance. This in-turn will work as seen earlier.
    1. It first uses the stringToItsLengthFunction function to map the string to its length (Integer).
    2. Finally, it uses the underlying equivalence instance which is Equivalence.equals to check for equivalence.
Function<Integer, String> integerToString = String::valueOf;
Equivalence<Integer> integerEquivalenceByLength = stringEquivalenceByLength
        .onResultOf(integerToString);

System.out.println(integerEquivalenceByLength.equivalent(1, 2)); //true
System.out.println(integerEquivalenceByLength.equivalent(11, 2)); //false

In the first call, the integers 1 and 2, get transformed to strings “1” and “2”. Then, they get transformed to their lengths, which gives us 1 and 1 (two Integers). When using the Equivalence.equals, we get true as the result.

In the second call, the Integer → String → Integer transformation chain will map (11, 2) → (”11”, “2”) → (2, 1). Hence, the equals based equivalence check will return false as 2 is not equal to 1.

Equivalence#wrap

The wrap method takes some object and wraps it and returns Equivalence.Wrapper. Wrapper is a static nested class which overrides the equals() and hashCode() by delegating to the Equivalence (the Equivalence instance of which we call the wrap() method).

In other words, for two instances a and b, wrap(a).equals(wrap(b)) is true if and only if equivalent(a, b) is true.

Let us use the Equivalence<String> which tests equivalence using the string lengths. After we wrap the two strings in Wrapper, calling equals() will delegate the call to the underlying Equivalence (stringEquivalenceByLength.equivalent(a, b)).

Function<String, Integer> stringToItsLengthFunction = String::length;
Equivalence<String> stringEquivalenceByLength = Equivalence.equals()
        .onResultOf(stringToItsLengthFunction);

String a = "abcd";
String b = "efgh";
Equivalence.Wrapper<String> wrapper1 = stringEquivalenceByLength.wrap(a);
Equivalence.Wrapper<String> wrapper2 = stringEquivalenceByLength.wrap(b);
System.out.println(wrapper1.equals(wrapper2)); //true
//same as
System.out.println(stringEquivalenceByLength.equivalent(a, b)); //true

When passing two strings of different length, it returns a false.

System.out.println(stringEquivalenceByLength.wrap("ab")
                .equals(stringEquivalenceByLength.wrap("c"))); //false

Equivalence#pairwise

When calling the pairwise() method on an Equivalence instance, it returns an equivalence over iterables based on the equivalence of their elements.

Its method signature is as follows, where T is the type parameter of the Equivalence and it returns a Equivalence<Iterable<S>> where S is a subtype of T.

public final <S extends @Nullable T> Equivalence<Iterable<S>> pairwise() { ... }

Two iterables are considered equivalent if they both contain the same number of elements, and each pair of corresponding elements is equivalent according to this equivalence instance.

Let’s say we have two List<String> and we call pairwise() on Equivalence.equals(). Thus, it iterates over both the lists (element by element) and passes them to the equivalent() method of the Equivalence (equals Equivalence).

List<String> l1 = List.of("a", "b", "c");
List<String> l2 = List.of("a", "b", "c");
Equivalence<Iterable<String>> iterableEquivalence = Equivalence.equals()
                .pairwise();
System.out.println(iterableEquivalence.equivalent(l1, l2)); //true

Maps#difference using an equivalence

Let us now use Maps#difference by passing an instance of Equivalence as the third argument, which determines how to compare values for equivalence.

Map<String, Integer> left = Map.of(
        "a", 1,
        "b", 2,
        "c", 3);
Map<String, Integer> right = Map.of(
        "b", 2,
        "c", 9,
        "d", 4);
MapDifference<String, Integer> diff = Maps.difference(left, right);
System.out.println(diff);

In the above code, we have two maps and we compute the difference normally. This results in the entry with key c ending up in the value difference category.

not equal: only on left={a=1}: only on right={d=4}: value differences={c=(3, 9)}

Let us now use the earlier used equivalence to compare integers based on the number of digits they have (by converting an Integer to a String and checking for its length).

Function<String, Integer> stringToItsLengthFunction = String::length;
Function<Integer, String> integerToString = String::valueOf;

Equivalence<Integer> integerEquivalenceByLength = Equivalence.equals()
        .onResultOf(stringToItsLengthFunction)
        .onResultOf(integerToString);

MapDifference<String, Integer> diff = Maps.difference(left, right, integerEquivalenceByLength);
System.out.println(diff);

Now, when comparing the values mapped to key c, it compares 3 with 9. Since both have the same length (1) and we used Equivalence.equals as the underlying equivalence instance, they will be considered as equivalent. This gives us the below result.

not equal: only on left={a=1}: only on right={d=4}

Maps#difference on a SortedMap

The last overloaded Maps#difference method works on a Java SortedMap. It returns a SortedMapDifference. A SortedMapDifference represents the differences between two sorted maps. The difference between a MapDifference and a SortedMapDifference is that the method return a SortedMap as shown below.

public interface SortedMapDifference<K extends @Nullable Object, V extends @Nullable Object>
    extends MapDifference<K, V> {

  @Override
  SortedMap<K, V> entriesOnlyOnLeft();

  @Override
  SortedMap<K, V> entriesOnlyOnRight();

  @Override
  SortedMap<K, V> entriesInCommon();

  @Override
  SortedMap<K, ValueDifference<V>> entriesDiffering();
}

This overloaded difference() method computes the difference between two sorted maps using the comparator of the left map (or Ordering.natural() if the left map uses the natural ordering of its elements).

Note from the Javadoc: Since this method uses TreeMap instances internally, the keys of the right map must all compare as distinct according to the comparator of the left map.

In the below example, the left map is a TreeMap with Comparator.reverseOrder. It thus orders the keys in reverse order. Hence, the ordering among entries in the result (MapDifference) will use this reverse comparator as shown below (Key e appears before key d).

SortedMap<String, Integer> left = new TreeMap<>(Comparator.reverseOrder());
left.putAll(Map.of(
        "a", 1,
        "b", 2,
        "c", 3));
Map<String, Integer> right = new HashMap<>(Map.of(
        "b", 2,
        "c", 9,
        "d", 4,
        "e", 5));
System.out.println("Left map: " + left);
System.out.println("Right map: " + right);
MapDifference<String, Integer> diff = Maps.difference(left, right);
System.out.println(diff);

Prints,

Left map: {c=3, b=2, a=1}
Right map: {b=2, c=9, d=4, e=5}
not equal: only on left={a=1}: only on right={e=5, d=4}: value differences={c=(3, 9)}

Conclusion

In this post, we explored what finding difference between two maps means. It includes the following categories – finding entries present only in the left/right map, the common entries and the entries with different values. We explored the Google Guava’s Maps#difference method to find these. We also learnt about the Google Guava’s Equivalence class in detail and used it to when comparing the values from the two maps.

Check out the other google-guava utility classes and feature.

Leave a Reply