Two customer records can describe the same customer while being two different objects. Kotlin provides separate operators for those questions: == asks about equality, while === asks whether two references identify the same object. Your class design determines what equality means for its instances.
The data classes lesson introduces generated equality. This guide follows that decision into sets and maps, where equality works together with hashCode. A key that changes its equality-relevant state after insertion can become difficult to retrieve even though the collection still holds the object.
Value meaning and object identity
For ordinary reference objects, === checks identity. Assigning an object to a second variable preserves that identity; constructing another object does not. The == operator uses the equals(Any?) contract and handles null safely. A class that inherits the default Any implementation generally compares by identity until it supplies different behavior.
A data class generates equals and hashCode from properties in its primary constructor. Properties declared only in the class body are excluded from those generated members. This can be useful for a cache or display state, but it is surprising if you expect such a field to distinguish records. Write tests documenting the intended boundary of value equality.
Avoid using identity to compare numeric or textual meaning. Runtime boxing and platform representation can make identity experiments misleading. State what the domain needs: two invoices may share an identifier even when their displayed descriptions differ, whereas two shopping-cart snapshots may be equal only when all their items match.
The hash contract connects equality to lookup
Hash-based collections use a hash value to narrow the candidate search, then use equality to distinguish matching keys. Equal objects must have equal hash codes. Unequal objects are allowed to share a hash code, so a collision alone is not an error or proof of equality.
If you override equals manually, override hashCode using the same equality-relevant fields. The equals signature must accept Any? to override the inherited function; an overload accepting only your own class does not redefine the language operator's contract. Good equality is reflexive, symmetric, transitive and consistent while the relevant state remains unchanged.
Never use a hash code as a permanent database identifier, password hash, security signature or collision-free fingerprint. Its purpose is collection placement, and ordinary object hashes are not a cryptographic promise. Use an explicit stable identifier for persistent records and an appropriate security API for cryptographic needs.
Mutable keys break expectations
Consider a data class with var id used as a map key. Inserting it places the entry according to its current hash. Changing id changes the generated hash and equality meaning, but the collection does not automatically remove and reinsert the entry. A later lookup may search somewhere different from the location chosen originally.
The practical rule is stronger than making the reference val. A val key variable prevents reassignment of that variable; it does not freeze var properties inside the object or mutable collections inside constructor properties. Design key objects with immutable, stable fields and avoid exposing equality-relevant mutable children.
If an identifier really changes, remove the old mapping before changing its key and insert a new immutable key deliberately. Better still, separate a record's stable identity from its editable details. A map from CustomerId to CustomerProfile makes it clear that changing a display name should not change where the profile is stored.
Arrays and shallow data class behavior
Arrays require explicit content comparison when you mean matching elements. contentEquals compares a flat array's elements, while contentDeepEquals supports nested arrays. Ordinary array == does not become element-by-element equality simply because the array appears inside a data class constructor.
Generated copy is also shallow: references to nested mutable objects can be shared between the original and its copy. Equality and copying are separate contracts, neither of which automatically creates deep immutability. For value-like models, immutable lists of immutable elements are often easier to reason about than arrays or mutable nested structures.
When reviewing a key type, ask which exact fields participate, whether their equality is content-based, and whether any participant can change. These questions are more useful than merely checking for the data keyword. They also reveal why a visually identical pair of objects might fail an equality assertion.
Official references and review
The official equality guide explains the language operators and array distinction. The hashCode API contract specifies consistency with equals. Before accepting a key type, check equal independently constructed keys, missing identifiers, body properties and nested mutability. Stable identity should survive ordinary updates to the record it identifies.
Kotlin Equality, hashCode and Map Keys Examples
Each example shows the code, the output it produces and what each part does.
Equal values, separate instances
A freshly constructed equal key retrieves the same mapping. The map does not require the exact object used during insertion. It relies on the key's equality and hashing behavior, which the data class supplies consistently for the immutable id property.
data class CustomerKey(val id: Int)
fun main() {
val first = CustomerKey(7)
val second = CustomerKey(7)
val alias = first
println(first == second)
println(first === second)
println(first === alias)
val names = mapOf(first to "Mina")
println(names[second])
}true
false
true
MinaKotlin Equality, hashCode and Map Keys in Real Projects
Worked exercise: Keep editable profiles out of keys
Build a directory whose customers can change their display names without affecting lookup. Use a stable immutable customer identifier as the key and store editable information as the value. Construct a new equal key during lookup to verify that retrieval depends on value equality.
data class CustomerId(val value: Int)
data class Profile(var displayName: String)
fun main() {
val key = CustomerId(12)
val directory = mutableMapOf(key to Profile("Mina"))
directory.getValue(key).displayName = "Mina K."
println(directory[CustomerId(12)]?.displayName)
println(directory.containsKey(CustomerId(99)))
println(key.value)
}Mina K.
false
12Editing the value leaves the key unchanged. In a production model, validate identifiers at construction and decide whether profile mutation itself should be restricted. Test equal keys, different keys and missing keys. Do not assert a particular numeric hash value: the contract is equality consistency, not a domain-specific hash number.
Kotlin Equality, hashCode and Map Keys FAQ
Does val make a map key immutable?
Only the variable binding is fixed by val. The object can still expose mutable properties, and constructor properties can reference mutable lists or other changing objects. If that state participates in equals and hashCode, mutation can invalidate lookup assumptions. Prefer a key type containing immutable scalar identifiers and inspect its nested values too. Immutability is a property of the relevant object graph, not just the declaration of the outer variable.
Are two objects equal when their hash codes match?
No. Hash codes are a compact aid to finding candidates, and multiple unequal objects can share one hash. The collection then relies on equals to distinguish them. The required direction is that equal objects must have equal hashes. Reversing that implication would treat collisions as identity and merge unrelated records. For tests, compare equality directly and verify consistent hashes for equal values without assuming every distinct value produces a unique number.
Why does a data class ignore one of my properties?
The generated equality members use properties declared in the primary constructor. A property placed in the class body is outside that generated value definition, even if it changes what the object displays. Decide whether the field is incidental state or part of the value. Move it into the constructor when appropriate, or implement a carefully tested custom equality contract. Do not expect copy or destructuring to include every body property either.
Should an entity compare all fields or just its identifier?
That is a domain decision. A stable identifier can make sense for a long-lived entity whose name or address changes, while a snapshot value may need every relevant field compared. Document the choice so callers do not mistake entity identity for identical current contents. Often separate types are clearer: CustomerId for lookup and CustomerSnapshot for value comparison. This separation avoids making editable business details unexpectedly control map-key placement.
Kotlin Equality, hashCode and Map Keys Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
Which design best supports changing a customer's display name?
Kotlin Equality, hashCode and Map Keys Quiz
2 questions to check what you learned.
What does first === second ask for ordinary reference objects?
- Whether both references identify the same object.
- Whether their hash codes collide.
- Whether their printable text is identical.
Which hash rule is required?
- A hash code must be a permanent unique customer identifier.
- Equal objects must have equal hash codes.
- Unequal objects must always have different hash codes.