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.

Why is this an issue?

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.

Code examples

Noncompliant code example

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

Compliant solution

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

Noncompliant code example

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"
  }
}

Compliant solution

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

Noncompliant code example

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"
  }
}

Compliant solution

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

Noncompliant code example

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

Compliant solution

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

Exceptions

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

Resources

Documentation

Related rules