Implementing equals() is hard
When modeling an entity that may or may-not have an id assigned, writing the equals method is surprisingly hard. What to do when there's no id? Falling back on equals-by-value is tempting:public class Product {
private final Id id;
private final String description;
private final Money price;
...
public boolean equals(Object other) {
if (!(other instanceof Product)) {
return false;
}
Product otherProduct = (Product) other;
if (id != null && otherProduct.id != null) {
return id.equals(otherProduct.id);
} else {
return description.equals(otherProduct.description)
&& price.equals(otherProduct.price);
}
}
}
Unfortunately, this doesn't work. The
equals()
method must be reflective, symmetric and transitive. For arbitrary values of a, b, and c, the following tests* must always pass: /* reflective */
assertTrue(a.equals(a));
/* symmetric */
assertTrue(a.equals(b) == b.equals(a));
/* transitive */
if (a.equals(b) && b.equals(c)) {
assertTrue(a.equals(c));
}
Transitivity is hard
Unfortunately, our example fails transitivity:
Product a = new Product(5, "Large Pepperoni", Money.usd(11));
Product b = new Product(null, "Large Pepperoni", Money.usd(11));
Product c = new Product(6, "Large Pepperoni", Money.usd(11));
This is a case where a equals b and b equals c, but a doesn't equal c.Consequences
Without a proper equals method, it's dangerous to use this Product class in Collections. For example, the behaviour of
HashMap.remove(b)
is poorly defined for a HashMap that contains both a and c.The Fix
The equals method should be defined by value or by id, but not either.