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

ImpactDetails
IntegrityScope: Integrity

Modify Application Data - Objects in hash-based collections behave unpredictably, causing data corruption, duplicates, or lost entries.
OtherScope: 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

  1. MITRE Corporation. "CWE-581: Object Model Violation: Just One of Equals and Hashcode Defined." https://cwe.mitre.org/data/definitions/581.html
  2. Joshua Bloch. "Effective Java" - Item 11: Always override hashCode when you override equals.
  3. Oracle. "Object.equals() and Object.hashCode() Documentation."