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.
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.
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.
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);
}
}
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.
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());
}
}
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());
}
}
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();
}
}
Object.hashCode MethodObject.equals
MethodObjects.hash
Method