Introduction
Given a class C, if you want to implement ordering, you have two choices:
- Have your class implement
Comparable<c>
(orComparable<x>
, whereX
is a superclass ofC
). - Define a
Comparator<c>
to use with this class (or aComparator<x>
, whereX
is a superclass ofC
).
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 instancesa
andb
of classC
, are those two classes considered equivalent? This is really like writing an implementation ofequals()
for classC
, 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 typeC
).doHash(C t)
: given an instancet
of classC
, compute a hash value. Of course, an implementation must mirrorObject
‘shashCode()
/equals()
contract: ifdoEquivalent(a, b)
istrue
, thendoHash(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!