Objektmodell-Verletzung: Nur equals oder hashCode definiert

Beschreibung

Objektmodell-Verletzung: Nur equals oder hashCode definiert ist ein Java-Programmierfehler, bei dem eine Klasse entweder die equals()-Methode oder die hashCode()-Methode überschreibt, aber nicht beide. Java-Objekte müssen einer fundamentalen Invariante gehorchen: Gleiche Objekte müssen gleiche Hashcodes haben. Mit anderen Worten, wenn a.equals(b) true zurückgibt, muss a.hashCode() gleich b.hashCode() sein. Wenn nur eine dieser Methoden überschrieben wird, wird diese Invariante verletzt, was unvorhersehbares Verhalten verursacht, wenn Objekte in Hash-basierten Collections wie HashMap, HashSet oder Hashtable verwendet werden.

Risiko

Die Verletzung des equals/hashCode-Vertrags erzeugt subtile aber ernsthafte Fehler. Objekte, die logisch gleich sind, können als Duplikate in HashSets gespeichert werden, weil ihre Hashcodes unterschiedlich sind. HashMap-Lookups finden Objekte nicht, selbst wenn gleiche Schlüssel existieren. Objekte "verschwinden" aus Collections, wenn sich ihre Felder nach dem Einfügen ändern. Datenkorruption tritt stillschweigend ohne offensichtliche Fehler auf, was Fehler extrem schwer zu diagnostizieren macht. In Sicherheitskontexten kann Authentifizierungs- oder Autorisierungslogik, die Hash-basierte Lookups verwendet, unvorhersehbar fehlschlagen und potenziell Sicherheitsprüfungen umgehen oder Denial-of-Service verursachen.

Lösung

Überschreiben Sie immer sowohl equals() als auch hashCode() zusammen. Bei der Implementierung von equals() schließen Sie alle Felder ein, die die logische Gleichheit bestimmen. Bei der Implementierung von hashCode() verwenden Sie dieselben Felder, die in equals() verwendet werden, um den Hash zu berechnen. Verwenden Sie IDE-generierte Implementierungen oder Objects.hash() für konsistente, korrekte Implementierungen. Erwägen Sie die Verwendung von Lomboks @EqualsAndHashCode-Annotation oder ähnlichen Codegenerierungswerkzeugen. Verwenden Sie unveränderliche Felder für Hash-Schlüssel-Berechnung wo möglich. Testen Sie beide Methoden zusammen, um zu verifizieren, dass der Vertrag eingehalten wird.

Häufige Auswirkungen

AuswirkungDetails
IntegritätBereich: Integrität

Anwendungsdaten modifizieren - Objekte in Hash-basierten Collections verhalten sich unvorhersehbar und verursachen Datenkorruption, Duplikate oder verlorene Einträge.
SonstigeBereich: Sonstige

Qualitätsverschlechterung - Anwendungen zeigen intermittierende, schwer reproduzierbare Fehler, wenn Hash-basierte Operationen nicht wie erwartet funktionieren.

Beispielcode

Verwundbarer Code

// Verwundbar: Nur equals() definiert, kein hashCode()
public class VulnerablePerson {
    private String name;
    private int age;

    public VulnerablePerson(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // Verwundbar: equals() ohne 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);
    }

    // Fehlendes hashCode() - verwendet Object.hashCode() das identitätsbasiert ist
}

// Demonstration des Problems
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) gibt true zurück
        System.out.println(person1.equals(person2));  // true

        // Aber sie haben unterschiedliche Hashcodes!
        System.out.println(person1.hashCode());  // z.B. 1234567
        System.out.println(person2.hashCode());  // z.B. 7654321

        people.add(person1);
        people.add(person2);

        // Beide werden hinzugefügt obwohl sie "gleich" sind!
        System.out.println(people.size());  // 2, sollte 1 sein!
    }
}

// Verwundbar: Nur hashCode() definiert, kein equals()
public class VulnerableProduct {
    private String sku;
    private String name;

    public VulnerableProduct(String sku, String name) {
        this.sku = sku;
        this.name = name;
    }

    // Verwundbar: hashCode() ohne equals()
    @Override
    public int hashCode() {
        return Objects.hash(sku, name);
    }

    // Fehlendes equals() - verwendet Object.equals() das identitätsbasiert ist
}

// Demonstration des Problems
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");

        // Gleicher Hashcode
        System.out.println(product1.hashCode() == product2.hashCode());  // true

        // Aber equals gibt false zurück (Identitätsvergleich)!
        System.out.println(product1.equals(product2));  // false

        inventory.put(product1, 100);

        // Kann den Eintrag mit "gleichem" Schlüssel nicht finden!
        Integer quantity = inventory.get(product2);
        System.out.println(quantity);  // null - Eintrag nicht gefunden!
    }
}

// Verwundbar: Inkonsistente equals und hashCode
public class VulnerableUser {
    private long id;
    private String username;
    private String email;

    // equals() vergleicht nur 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;  // Vergleicht nur id
    }

    // hashCode() verwendet username und email, nicht id!
    @Override
    public int hashCode() {
        return Objects.hash(username, email);  // Inkonsistent!
    }

    // Zwei Benutzer mit gleicher id aber unterschiedlichen Benutzernamen werden:
    // - Gleich sein (gleiche id)
    // - Unterschiedliche Hashcodes haben (unterschiedliche Benutzernamen)
    // Dies verletzt den Vertrag!
}

Lösungscode

// Behoben: Sowohl equals() als auch hashCode() ordnungsgemäß definiert
public class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // Behoben: equals() verwendet name und 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);
    }

    // Behoben: hashCode() verwendet dieselben Felder wie equals()
    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}

// Behoben: Ordnungsgemäße Produktklasse
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);
    }
}

// Behoben: Konsistente equals und hashCode
public class User {
    private long id;
    private String username;
    private String email;

    // Behoben: Beide Methoden verwenden dieselben Felder
    @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);  // Gleiches Feld wie equals()
    }
}

// Behoben: Java-Records verwenden (Java 16+) - generiert equals/hashCode automatisch
public record PersonRecord(String name, int age) {
    // equals() und hashCode() automatisch korrekt generiert
}

// Behoben: Lombok verwenden
import lombok.EqualsAndHashCode;

@EqualsAndHashCode
public class Employee {
    private String employeeId;
    private String name;
    private String department;

    // Lombok generiert konsistente equals() und hashCode()
}

// Behoben: Lombok mit Feldauswahl verwenden
@EqualsAndHashCode(of = {"employeeId"})
public class EmployeeById {
    private String employeeId;
    private String name;  // Nicht in equals/hashCode enthalten
    private String department;  // Nicht enthalten
}

// Behoben: Umfassendes Beispiel mit Vererbung
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);
    }
}

// Verifikationstest
public class EqualsHashCodeTest {
    public static void main(String[] args) {
        Person p1 = new Person("John", 30);
        Person p2 = new Person("John", 30);

        // Vertrag verifizieren
        assert p1.equals(p2);
        assert p1.hashCode() == p2.hashCode();

        // Collection-Verhalten verifizieren
        Set<Person> set = new HashSet<>();
        set.add(p1);
        set.add(p2);
        assert set.size() == 1;  // Nur ein Eintrag

        Map<Person, String> map = new HashMap<>();
        map.put(p1, "first");
        assert map.get(p2).equals("first");  // Kann mit gleichem Schlüssel abrufen

        System.out.println("Alle Assertions bestanden!");
    }
}

CVE-Beispiele

Keine spezifischen CVEs werden dieser CWE üblicherweise zugeordnet, da sie primär die Anwendungskorrektheit betrifft statt Sicherheitsschwachstellen.


Referenzen

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