Kotlin / Editorial solution guide
Diagnose shallow copies and unstable equality
Investigate changed snapshots, mutable aliases, data-class constructor fields, and hash-based lookup failures.
Read-only knowledge base · No public comments
Recognize the symptoms
A fictional screen copies a data-class state, modifies the copied list, and unexpectedly changes the original. Elsewhere, a record placed in a hash-based collection becomes difficult to find after a property changes. These symptoms involve object identity and value contracts, not a failure of val syntax.
Understand the likely causes
- Data-class copy is shallow, read-only references can share mutable objects, and generated equality/hashCode depend on primary-constructor properties. Mutating equality-defining state while it is used as a hash key can violate the collection’s lookup assumptions. Body-only properties can also be omitted from generated equality unexpectedly.
Draw the references before changing the model
Name the original object, copied object, and every mutable nested value. Ask whether copy created a new container or simply reused a property reference. A val property does not prevent a referenced list from changing, and List as an interface does not prove another alias cannot mutate it. Use synthetic values to demonstrate the alias without exposing user state. The example deliberately shows an undesirable shared mutation; its expected output is analytical and was not obtained from a Kotlin execution.
data class Draft(val labels: MutableList<String>)
fun main() {
val original = Draft(mutableListOf("basic"))
val copied = original.copy()
copied.labels.add("extra")
println(original.labels.joinToString("|"))
println(original.labels === copied.labels)
}
basic|extra
true
Expected lines are basic|extra and true. The Draft instances differ, but their labels properties reference the same mutable list. This is not a safe snapshot pattern and was not compiled.
Identify exactly which fields define the value
Inspect the primary constructor and any custom equals or hashCode implementation. Data-class body properties are not automatically included in generated value operations. Decide whether a field represents stable identity, mutable presentation state, or a derived cache. Do not add every field blindly: equality with expensive mutable graphs can create new correctness and cost problems. For a hash key, use stable value-defining properties and keep mutation outside the key object. A lookup failure after mutation should prompt a model review, not a retry loop around the collection operation.
Choose a copy and ownership policy that matches the promise
For string elements, copying the list structure is enough to isolate later additions and removals. For mutable element objects, a second level of copying or an immutable element model may be needed. The corrected example creates a separate list and exposes it as a read-only property, but callers still need an ownership discipline because read-only is not deep immutability. Prefer immutable records for published state when practical. Keep any deep-copy policy explicit about cycles, resources, identity, and nested mutable values rather than claiming that one copy call clones an entire object graph.
data class Snapshot(val labels: List<String>)
fun main() {
val source = mutableListOf("basic")
val first = Snapshot(source.toList())
source.add("extra")
val second = first.copy(labels = first.labels + "reviewed")
println(first.labels.joinToString("|"))
println(second.labels.joinToString("|"))
}
basic
basic|reviewed
Expected output is basic then basic|reviewed. The source mutation does not change first because its list structure was copied. String elements avoid deep-mutation issues in this fixture. Unexecuted source.
Verify both content isolation and value behavior
In an isolated compatible project, mutate the source container after snapshot creation, mutate any permitted nested elements, and compare before/after values. For map or set keys, verify equality and hash stability for the operations the application permits. Check data-class body fields separately to confirm the intended comparison policy. Do not rely on a pretty toString or a single == assertion as proof of isolation. If published state is shared across coroutines, add a separate ownership and publication review; copying alone does not establish concurrency correctness.
Verify the fix
The original mutation should no longer alter a promised snapshot, and equality/hash behavior must remain consistent with collection use. Record the exact toolchain and observations. These source fixtures were not compiled, so no runtime pass or race-safety guarantee is claimed here.
Keep it from recurring
- Use stable value types for keys, keep mutable aliases out of snapshot APIs, and review generated data-class operations whenever constructor fields change. Document shallow-copy behavior and test representative nested state rather than assuming val or copy provides deep immutability.