This rule raises an issue when hashCode() reads an instance field that equals() does not read. Two objects that compare equal can then produce different hash codes.

Why is this an issue?

The Object.hashCode() contract requires that objects considered equal by equals() produce the same hash code. When hashCode() factors in a field that equals() ignores, two instances that equals() treats as identical can still return different hash codes, because that extra field is free to differ between them.

That defect is easy to introduce and easy to miss in review: adding a field to a class and remembering to include it in hashCode()’s aggregation while forgetting to add the matching comparison in `equals() still compiles and looks complete.

Breaking the contract has consequences that only surface at runtime, and often only for specific data. A HashMap or HashSet computes an object’s bucket from its hash code before ever calling equals(). If an equal object later has a different hash code, it lands in a different bucket, so lookups, deduplication, and cache retrieval silently fail to find it.

The reverse situation, a field compared in equals() but not read in hashCode(), does not break this contract: hashCode() is still free to return the same value for those objects. It can reduce how well hash codes spread across buckets, but it is not a correctness defect, so this rule does not flag it.

How to fix it

Either add the missing field to equals(), or remove it from hashCode().

Adding the field to equals() is usually the safer choice: it keeps the field as part of the object’s identity and restores the contract without discarding information that hashCode() was already using. Removing the field from hashCode() is appropriate only when that field is not actually part of what makes two instances equal.

Code examples

Noncompliant code example

final class OrderKey {
  private final long id;
  private final int version;

  @Override
  public boolean equals(Object other) {
    return other instanceof OrderKey key && id == key.id;
  }

  @Override
  public int hashCode() { // Noncompliant: reads "version", which equals() never reads
    return Objects.hash(id, version);
  }
}

Compliant solution

final class OrderKey {
  private final long id;
  private final int version;

  @Override
  public boolean equals(Object other) {
    return other instanceof OrderKey key && id == key.id && version == key.version;
  }

  @Override
  public int hashCode() {
    return Objects.hash(id, version);
  }
}

Simple getters that only return a field are resolved the same way as direct field reads.

Noncompliant code example

final class Person {
  private final String firstName;
  private final String lastName;

  String getFirstName() {
    return firstName;
  }

  String getLastName() {
    return lastName;
  }

  @Override
  public boolean equals(Object other) {
    return other instanceof Person person && getFirstName().equals(person.getFirstName());
  }

  @Override
  public int hashCode() { // Noncompliant: reads "lastName", which equals() never reads
    return Objects.hash(getFirstName(), getLastName());
  }
}

Compliant solution

final class Person {
  private final String firstName;
  private final String lastName;

  String getFirstName() {
    return firstName;
  }

  String getLastName() {
    return lastName;
  }

  @Override
  public boolean equals(Object other) {
    return other instanceof Person person
      && getFirstName().equals(person.getFirstName())
      && getLastName().equals(person.getLastName());
  }

  @Override
  public int hashCode() {
    return Objects.hash(getFirstName(), getLastName());
  }
}

Exceptions

This rule only compares equals(Object) and hashCode() when both are declared directly in the same class. It does not analyze inherited implementations, generated ones such as a record’s or a Lombok-annotated class’s, or static fields.

A field that only caches a previously computed hash value is not flagged, because that field does not add new identity state beyond what equals() already compares:

final class Point {
  private final int x;
  private final int y;
  private int cachedHash;

  @Override
  public boolean equals(Object other) {
    return other instanceof Point point && x == point.x && y == point.y;
  }

  @Override
  public int hashCode() {
    if (cachedHash == 0) {
      cachedHash = Objects.hash(x, y);
    }
    return cachedHash;
  }
}

The rule does not raise an issue when hashCode() delegates to a method whose field reads it cannot resolve, such as one that calls another type’s methods or applies further logic beyond returning a field:

final class Config {
  private final Map<String, String> values;

  @Override
  public boolean equals(Object other) {
    return other instanceof Config config && values.equals(config.values);
  }

  @Override
  public int hashCode() {
    return computeHash();
  }

  private int computeHash() {
    return values.entrySet().stream().mapToInt(Object::hashCode).sum();
  }
}

Resources

Documentation

Related rules