1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151
|
Consider the following code:
```java
String x = "42";
Integer y = 42;
if (x.equals(y)) {
System.out.println("What is this, Javascript?");
} else {
System.out.println("Types have meaning here.");
}
```
We understand that any `Integer` will *not* be equal to any `String`. However,
the signature of the `equals` method accepts any Object, so the compiler will
happily allow us to pass an Integer to the equals method. However, it will
always return false, which is probably not what we intended.
This check detects circumstances where the equals method is called when the two
objects in question can *never* be equal to each other. We check the following
equality methods:
* `java.lang.Object.equals(Object)`
* `java.util.Objects.equals(Object, Object)`
* `com.google.common.base.Objects.equal(Object, Object)`
## I'm trying to test to make sure my equals method works
Good! Many tests of equals methods neglect to test that equals on an unrelated
object return false.
We recommend using Guava's [EqualsTester][equalstester] to perform tests of your
equals method. Simply give it a collection of objects of your class, broken into
groups that should be equal to each other, and EqualsTester will ensure that:
* Each object is equal to each other object in the same group as that object
* Each object is equal to itself
* Each object is unequal to all of the other objects not in the group
* Each object is unequal to an unrelated object (Relevant to this check)
* Each object is unequal to null
* The `hashCode` of each object in a group is the same as the hash code of
each other member of the group
Which should exhaustively check all of the properties of `equals` and
`hashCode`.
## But I'm doing something funky with my equals method!
The javadoc of [`Object.equals(Object)`][objeq] defines object equality very
precisely:
> The equals method implements an equivalence relation on non-null object
> references:
>
> It is reflexive: for any non-null reference value x, x.equals(x) should return
> true.
>
> It is symmetric: for any non-null reference values x and y, x.equals(y) should
> return true if and only if y.equals(x) returns true.
>
> It is transitive: for any non-null reference values x, y, and z, if
> x.equals(y) returns true and y.equals(z) returns true, then x.equals(z) should
> return true.
>
> It is consistent: for any non-null reference values x and y, multiple
> invocations of x.equals(y) consistently return true or consistently return
> false, provided no information used in equals comparisons on the objects is
> modified.
>
> For any non-null reference value x, x.equals(null) should return false.
TIP: [EqualsTester][equalstester] validates each of these properties.
For most simple value objects (e.g.: a `Point` containing `x` and `y`
coordinates), this generally means that the equals method will only return true
if the other object has the exact same class, and each of the components is
equal to the corresponding component in the other object. Here, there are
numerous tools in the Java ecosystem to generate the appropriate `equals` and
`hashCode` method implementations, including [AutoValue][av].
Another pattern often seen is to declare a common supertype with a defined
`equals` method (like `List`, which defines equality by having equal elements in
the same order). Then, different subclasses of that supertype (`LinkedList` and
`ArrayList`) can be equal to other classes with that supertype, since the
concrete class of the `List` is irrelevant. This checker will allow these types
of equality, as we detect when two objects share a common supertype with an
`equals` implementation and allow that to succeed.
Outside of these two general groups of equals methods, however, it's very
difficult to produce correctly-behaving equals methods. Most of the time, when
`equals` is implemented in a non-obvious manner, one or more of the properties
above isn't satisfied (generally the symmetric property). This can result in
subtle bugs, explained below.
### A bad example of `equals()`
```java
class Foo {
private String foo; // Some property
public boolean equals(Object other) {
if (other instanceof String) {
return other.equals(foo); // We want to be able to call equals with a String
}
if (other instanceof Foo) {
return ((Foo) other).foo.equals(foo); // Simplified, avoid null checks
}
return false;
}
public int hashCode() {
return foo.hashCode();
}
}
```
Here, `Foo`'s equals method is defined to accept a `String` value in addition to
other `Foo`'s. This may appear to work at first, but you end up with some
complex situations:
```java
Foo a = new Foo("hello");
Foo b = new Foo("hello");
String hi = "hello";
if (a.equals(b)) {
System.out.println("yes"); // Is printed, expected
}
if (b.equals(hi)) {
System.out.println("yes"); // Is printed, abusing equals
}
if (hi.equals(b)) {
System.out.println("no"); // Isn't printed, since String doesn't equals() Foo
}
Set<Foo> set = new HashSet<Foo>();
set.add(a);
set.add(b);
if (set.contains(hi)) {
// Maybe? Depends on which way HashSet decides to call .equals()
System.out.println("contained");
// Is it removed? It's not guaranteed to be, since the .equals() method could
// be called the other way in the remove path. Object.equals documentation
// specifies it's supposed to be symmetric, so this could work.
boolean removed = set.remove(hi);
}
```
[equalstester]: http://static.javadoc.io/com.google.guava/guava-testlib/19.0/com/google/common/testing/EqualsTester.html
[objeq]: https://docs.oracle.com/javase/7/docs/api/java/lang/Object.html#equals(java.lang.Object)
[av]: https://github.com/google/auto/blob/master/value/userguide/index.md
|