Comparing a field or getter of this with a different field or getter of the other instance inside equals is almost always
a copy-paste error. Replace the mismatched member with the corresponding one on the other instance.
An equals implementation usually compares each piece of state on this with the same piece of state on the other object.
When the two operands name different members of the enclosing type, objects that represent the same value can compare as unequal, and objects that
differ can compare as equal. That defect is easy to miss in review because the surrounding method still looks like a normal field-by-field
comparison.
This rule raises an issue when equals compares two different instance fields, or two different instance getters, of the enclosing
type. The comparison may use operators such as == or helpers such as Objects.equals.
class User {
private String firstName;
private String lastName;
@Override
public boolean equals(Object obj) {
if (obj instanceof User other) {
return Objects.equals(this.firstName, other.firstName)
&& Objects.equals(this.lastName, other.firstName); // Noncompliant: compares "lastName" with "firstName"
}
return false;
}
}
class User {
private String firstName;
private String lastName;
@Override
public boolean equals(Object obj) {
if (obj instanceof User other) {
return Objects.equals(this.firstName, other.firstName)
&& Objects.equals(this.lastName, other.lastName);
}
return false;
}
}
class Point {
private int x;
private int y;
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Point that)) {
return false;
}
return x == that.x && y == that.x; // Noncompliant: compares "y" with "x"
}
}
class Point {
private int x;
private int y;
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Point that)) {
return false;
}
return x == that.x && y == that.y;
}
}
class Box {
private Object key;
private Object value;
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Box that)) {
return false;
}
return key.equals(that.key) && value.equals(that.key); // Noncompliant: compares "value" with "key"
}
}
class Box {
private Object key;
private Object value;
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Box that)) {
return false;
}
return key.equals(that.key) && value.equals(that.value);
}
}
class Person {
private String firstName;
private String lastName;
String getFirstName() {
return firstName;
}
String getLastName() {
return lastName;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Person that)) {
return false;
}
return getFirstName().equals(that.getFirstName())
&& getLastName().equals(that.getFirstName()); // Noncompliant: compares "getLastName()" with "getFirstName()"
}
}
class Person {
private String firstName;
private String lastName;
String getFirstName() {
return firstName;
}
String getLastName() {
return lastName;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Person that)) {
return false;
}
return getFirstName().equals(that.getFirstName())
&& getLastName().equals(that.getLastName());
}
}
The rule does not raise an issue when both swapped pairings appear in the same statement, because that pattern can express order-independent equality. An undirected connection is the same in either direction:
class UndirectedEdge {
private String from;
private String to;
@Override
public boolean equals(Object obj) {
if (!(obj instanceof UndirectedEdge that)) {
return false;
}
return (from.equals(that.from) && to.equals(that.to))
|| (from.equals(that.to) && to.equals(that.from));
}
}
The rule also does not raise an issue when a field is compared with a getter, because the two operands are different kinds of members:
class Named {
private String name;
String getName() {
return name;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Named that)) {
return false;
}
return this.name.equals(that.getName());
}
}
When a mismatched comparison is intentional and those automatic exceptions do not apply, suppress the issue with a NOSONAR comment and
a short rationale on that line. Keep both swapped pairings in one statement when you can; use NOSONAR when the reverse pairing lives in
another statement, or when the domain truly requires a one-sided comparison:
class UndirectedEdge {
private String from;
private String to;
@Override
public boolean equals(Object obj) {
if (!(obj instanceof UndirectedEdge that)) {
return false;
}
if (from.equals(that.from) && to.equals(that.to)) {
return true;
}
return from.equals(that.to) && to.equals(that.from); // NOSONAR: undirected edge, reverse pairing is on the previous return
}
}
Object.equals
MethodObjects.equals Method