Unpaired Equals / HashCode

ID

kotlin.unpaired_equals_hashcode

Severity

high

Remediation Complexity

medium

Remediation Risk

low

Remediation Effort

medium

Resource

Code Smell

Language

Kotlin

Tags

correctness, reliability

Description

Reports classes that override equals() without also overriding hashCode(), or that override hashCode() without also overriding equals(). Data classes are excluded because Kotlin auto-generates both methods for them.

Rationale

The Any contract requires that objects which are equal under equals() must return the same value from hashCode(). Breaking this contract causes hash-based collections (HashMap, HashSet, LinkedHashMap) to silently malfunction: two logically equal objects may land in different buckets and the collection treats them as distinct.

// Bad — equals only
class User(val id: Int) {
    override fun equals(other: Any?) = other is User && id == other.id
    // hashCode not overridden — two equal Users have different hash codes
}

val set = hashSetOf(User(1))
set.contains(User(1))  // returns false — same id, different bucket

// Good — both overridden
class User(val id: Int) {
    override fun equals(other: Any?) = other is User && id == other.id
    override fun hashCode() = id.hashCode()
}

// Best — use a data class
data class User(val id: Int)

Remediation

Override both equals() and hashCode() together. Use a data class when the class is a simple value holder — Kotlin generates a correct and consistent implementation automatically. If manual implementation is required, use Objects.hash(…​) to compose hash codes from the same fields used in equals().