Object Model Violation: Just One of Equals and Hashcode Defined
Description
Object Model Violation: Just One of Equals and Hashcode Defined is a Java programming error where a class overrides either the equals() method or the hashCode() method but not both. Java objects are expected to obey a fundamental invariant: equal objects must have equal hashcodes. In other words, if a.equals(b) returns true, then a.hashCode() must equal b.hashCode(). When only one of these methods is overridden, this invariant is violated, causing unpredictable behavior when objects are used in hash-based collections like HashMap, HashSet, or Hashtable.
Risk
Violating the equals/hashCode contract creates subtle but serious bugs. Objects that are logically equal may be stored as duplicates in HashSets because their hashcodes differ. HashMap lookups fail to find objects even when equal keys exist. Objects "disappear" from collections when their fields change after insertion. Data corruption occurs silently without obvious errors, making bugs extremely difficult to diagnose. In security contexts, authentication or authorization logic using hash-based lookups may fail unpredictably, potentially bypassing security checks or causing denial of service.
Solution
Always override both equals() and hashCode() together. When implementing equals(), include all fields that determine logical equality. When implementing hashCode(), use the same fields used in equals() to compute the hash. Use IDE-generated implementations or Objects.hash() for consistent, correct implementations. Consider using Lombok's @EqualsAndHashCode annotation or similar code generation tools. Use immutable fields for hash key computation when possible. Test both methods together to verify the contract is maintained.
Common Consequences
| Impact | Details |
|---|---|
| Integrity | Scope: Integrity Modify Application Data - Objects in hash-based collections behave unpredictably, causing data corruption, duplicates, or lost entries. |
| Other | Scope: Other Quality Degradation - Applications exhibit intermittent, hard-to-reproduce failures when hash-based operations don't work as expected. |
Example Code
Vulnerable Code
// Vulnerable: Only equals() defined, no hashCode()
public class VulnerablePerson {
private String name;
private int age;
public VulnerablePerson(String name, int age) {
this.name = name;
this.age = age;
}
// Vulnerable: equals() without hashCode()
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
VulnerablePerson other = (VulnerablePerson) obj;
return age == other.age &&
Objects.equals(name, other.name);
}
// Missing hashCode() - uses Object.hashCode() which is identity-based
}
// Demonstration of the problem
public class HashSetProblem {
public static void main(String[] args) {
Set<VulnerablePerson> people = new HashSet<>();
VulnerablePerson person1 = new VulnerablePerson("John", 30);
VulnerablePerson person2 = new VulnerablePerson("John", 30);
// person1.equals(person2) returns true
System.out.println(person1.equals(person2)); // true
// But they have different hashcodes!
System.out.println(person1.hashCode()); // e.g., 1234567
System.out.println(person2.hashCode()); // e.g., 7654321
people.add(person1);
people.add(person2);
// Both are added despite being "equal"!
System.out.println(people.size()); // 2, should be 1!
}
}
// Vulnerable: Only hashCode() defined, no equals()
public class VulnerableProduct {
private String sku;
private String name;
public VulnerableProduct(String sku, String name) {
this.sku = sku;
this.name = name;
}
// Vulnerable: hashCode() without equals()
@Override
public int hashCode() {
return Objects.hash(sku, name);
}
// Missing equals() - uses Object.equals() which is identity-based
}
// Demonstration of the problem
public class HashMapProblem {
public static void main(String[] args) {
Map<VulnerableProduct, Integer> inventory = new HashMap<>();
VulnerableProduct product1 = new VulnerableProduct("SKU001", "Widget");
VulnerableProduct product2 = new VulnerableProduct("SKU001", "Widget");
// Same hashcode
System.out.println(product1.hashCode() == product2.hashCode()); // true
// But equals returns false (identity comparison)!
System.out.println(product1.equals(product2)); // false
inventory.put(product1, 100);
// Cannot find the entry with "equal" key!
Integer quantity = inventory.get(product2);
System.out.println(quantity); // null - entry not found!
}
}
// Vulnerable: Inconsistent equals and hashCode
public class VulnerableUser {
private long id;
private String username;
private String email;
// equals() compares only id
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
VulnerableUser other = (VulnerableUser) obj;
return id == other.id; // Only compares id
}
// hashCode() uses username and email, not id!
@Override
public int hashCode() {
return Objects.hash(username, email); // Inconsistent!
}
// Two users with same id but different usernames will:
// - Be equal (same id)
// - Have different hashcodes (different usernames)
// This violates the contract!
}
Fixed Code
// Fixed: Both equals() and hashCode() properly defined
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// Fixed: equals() using name and age
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Person other = (Person) obj;
return age == other.age &&
Objects.equals(name, other.name);
}
// Fixed: hashCode() using same fields as equals()
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
// Fixed: Proper product class
public class Product {
private String sku;
private String name;
public Product(String sku, String name) {
this.sku = sku;
this.name = name;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Product other = (Product) obj;
return Objects.equals(sku, other.sku) &&
Objects.equals(name, other.name);
}
@Override
public int hashCode() {
return Objects.hash(sku, name);
}
}
// Fixed: Consistent equals and hashCode
public class User {
private long id;
private String username;
private String email;
// Fixed: Both methods use same field(s)
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
User other = (User) obj;
return id == other.id;
}
@Override
public int hashCode() {
return Objects.hash(id); // Same field as equals()
}
// Alternative: Use all relevant fields in both
// @Override
// public boolean equals(Object obj) {
// if (this == obj) return true;
// if (obj == null || getClass() != obj.getClass()) return false;
// User other = (User) obj;
// return id == other.id &&
// Objects.equals(username, other.username) &&
// Objects.equals(email, other.email);
// }
//
// @Override
// public int hashCode() {
// return Objects.hash(id, username, email);
// }
}
// Fixed: Using Java records (Java 16+) - auto-generates equals/hashCode
public record PersonRecord(String name, int age) {
// equals() and hashCode() automatically generated correctly
}
// Fixed: Using Lombok
import lombok.EqualsAndHashCode;
@EqualsAndHashCode
public class Employee {
private String employeeId;
private String name;
private String department;
// Lombok generates consistent equals() and hashCode()
}
// Fixed: Using Lombok with field selection
@EqualsAndHashCode(of = {"employeeId"})
public class EmployeeById {
private String employeeId;
private String name; // Not included in equals/hashCode
private String department; // Not included
}
// Fixed: Comprehensive example with inheritance
public class BaseEntity {
protected Long id;
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
BaseEntity other = (BaseEntity) obj;
return Objects.equals(id, other.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
public class Order extends BaseEntity {
private String orderNumber;
private LocalDate orderDate;
@Override
public boolean equals(Object obj) {
if (!super.equals(obj)) return false;
Order other = (Order) obj;
return Objects.equals(orderNumber, other.orderNumber) &&
Objects.equals(orderDate, other.orderDate);
}
@Override
public int hashCode() {
return Objects.hash(super.hashCode(), orderNumber, orderDate);
}
}
// Verification test
public class EqualsHashCodeTest {
public static void main(String[] args) {
Person p1 = new Person("John", 30);
Person p2 = new Person("John", 30);
// Verify contract
assert p1.equals(p2);
assert p1.hashCode() == p2.hashCode();
// Verify collection behavior
Set<Person> set = new HashSet<>();
set.add(p1);
set.add(p2);
assert set.size() == 1; // Only one entry
Map<Person, String> map = new HashMap<>();
map.put(p1, "first");
assert map.get(p2).equals("first"); // Can retrieve with equal key
System.out.println("All assertions passed!");
}
}
CVE Examples
No specific CVEs are commonly attributed to this CWE, as it primarily affects application correctness rather than security vulnerabilities.
References
- MITRE Corporation. "CWE-581: Object Model Violation: Just One of Equals and Hashcode Defined." https://cwe.mitre.org/data/definitions/581.html
- Joshua Bloch. "Effective Java" - Item 11: Always override hashCode when you override equals.
- Oracle. "Object.equals() and Object.hashCode() Documentation."