Guava's Equivalence: strategy for equals()/hashCode()

Introduction

Given a class C, if you want to implement ordering, you have two choices:

  1. Have your class implement Comparable<c> (or Comparable<x>, where X is a superclass of C).
  2. Define a Comparator<c> to use with this class (or a Comparator<x>, where X is a superclass of C).

In fact, many classes in the JDK will have you supply a Comparator if your class does not implement Comparable; examples include Collections.sort() and Arrays.sort().
It can be said that for a given class C, a Comparator defines a strategy for ordering, and that you need to supply a Comparator if the class itself does not define this strategy (that is, does not implement Comparable).
And while the JDK offers a means to provide different strategies for ordering, it does not do so for a more fundamental contract of Object: equals() and hashCode().
And this is where Guava comes in.

Equivalence: a strategy for Object’s `equals()` and `hashCode()`

Guava’s Equivalence intends to fill this gap. In the same vein as a Comparator, using an Equivalence allows you to either define a different equals()/hashCode() strategy than the one already defined by your target class, or to define one for a target class which “has none at all” (meaning, in this case, that the class uses Object‘s equals()/hashCode() implementations).

Usage part 1: implementing an Equivalence

Equivalence is a parameterized class; for a class C, you will need to implement two methods:

  • doEquivalent(C a, C b): given two instances a and b of class C, are those two classes considered equivalent? This is really like writing an implementation of equals() for class C, except that you don’t have to handle nulls (it is guaranteed that both parameters are non-null) nor casts (it is guaranteed that both arguments are “at least” of type C).
  • doHash(C t): given an instance t of class C, compute a hash value. Of course, an implementation must mirror Object‘s hashCode()/equals() contract: if doEquivalent(a, b) is true, then doHash(a) == doHash(b).

Note that it is guaranteed that arguments to these methods will never be null.

Usage part 2: “out of Collections” usage

There is really only one method you will need here: .equivalent(). Provided you want to test equivalence between two instances of a given class C, you will do:

final C a = ...;
final C b = ...;
final Equivalence<C> eq = ...;
// Test equivalence of a and b against equivalence stragey eq
if (eq.equivalent(a, b)) {
    // Yup, they are
}

Usage part 3: Collection usage

Unlike the Comparable/Comparator relationship, equivalence between objects has to be “engraved” into collections. This unfortunately means that the syntax to initiate a collection is somewhat on the verbose side. Namely, if you want to initiate a Set of elements of class C wrapped into an Equivalence, you will have to initialize it as such:

// Java 7 and better...
Set<Equivalence.Wrapper<C>> set = new HashSet<>();
// Java 5 or 6; and since we use Guava...
Set<Equivalence.Wrapper<C>> set = Sets.newHashSet();

You will also need to rely on an Equivalence implementation in order to interact with this collection (of course, you also need to ensure that you use the same implementation all along!):

// Your Equivalence...
Equivalence<C> eq = ...;
// Inserting an element c into a Set<Equivalence.Wrapper<C>> set
set.add(eq.wrap(c));
// Removing, testing...
set.remove(eq.wrap(c));
set.contains(eq.wrap(c));
// Retrieve the original element
C element;
for (final Equivalence.Wrapper<C> wrapped: set) {
    // get the original element
    element = wrapped.get();
    // do something with element
}

Conclusion

Admittedly, having to redefine an equivalence strategy is far less common than having to redefine an ordering strategy. It is, however, a welcome tool to use when you have to deal with a “foreign” class which doesn’t meet your equals()/hashCode() expectations, either because there is no implementation at all for this class, or because the existing implementations don’t suit your needs.
Happy coding!