EqualsIncompatibleType
An equality test between objects with incompatible types always returns false

Severity
WARNING

The problem

Consider the following code:

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:

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 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:

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) 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 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.

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()

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:

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);
}

Suppression

Suppress false positives by adding the suppression annotation @SuppressWarnings("EqualsIncompatibleType") to the enclosing element.