When designing a public type, what subtle pitfalls arise around the `Any` members `equals`, `hashCode`, and `toString` — especially for sensitive data and arrays?
answer
- data-class toString prints secrets
- Array equals = identity → contentEquals
- Mutable key fields break hashCode
- Override equals ⇒ override hashCode
- toString runs in logs/templates — keep it total
basics
~10 sDefault toString leaks little, but data class toString prints every field — including secrets. Arrays compare by identity, not content. And mutable fields used in hashCode corrupt hash-map keys. Override these members carefully.
solid answer
~40 sThe `Any` members have traps: (1) `data class` auto-`toString` prints **all** constructor properties, so a `data class Credentials(val token: String)` leaks the token into logs — override `toString` to redact. (2) `Array<T>`'s `equals`/`hashCode` are **referential** (from `Any`), so two arrays with identical contents are not `==`; use `contentEquals`/`contentHashCode` (or `contentDeepEquals`) instead, and never put a raw array in a `data class` if you expect value semantics. (3) Using **mutable** properties in `hashCode` breaks `HashMap`/`HashSet`: if a stored key mutates, its bucket no longer matches and lookups fail. (4) Overriding `equals` without `hashCode` (or vice-versa) violates the contract. (5) `toString` is called by string templates and loggers, so an expensive or exception-throwing `toString` can surprise you. Prefer immutable value types, redact secrets, and use content-based array APIs.
code
kotlin · 5 linesdata class ApiKey(val value: String) {
override fun toString() = "ApiKey(****)" // never log the raw key
}
val a = intArrayOf(1,2); val b = intArrayOf(1,2)
check(!a.equals(b)); check(a.contentEquals(b)) // arrays: use contentEqualsgo deeper
Knows toString prints fields and that overriding equals needs hashCode too.
Identifies array identity-equality and the need for contentEquals/contentHashCode.
Anticipates secret leakage, mutable-key hash corruption, and keeps toString cheap and total.
Builds team-wide conventions: immutable value types, secret-wrapper types, lint rules for array equality and unredacted logging.
## 1. `toString` leaks via data classes `data class` generates `toString()` listing **every primary-constructor property**: ```kotlin data class Login(val user: String, val password: String) println(Login("ada", "s3cr3t")) // Login(user=ada, password=s3cr3t) <-- leak! ``` String templates and most loggers call `toString` implicitly, so secrets land in logs. **Fix:** override `toString` to redact, or wrap secrets in a type whose `toString` masks them: ```kotlin data class Login(val user: String, val password: String) { override fun toString() = "Login(user=$user, password=***)" } ``` ## 2. Arrays use identity equality `Array<T>` (and `IntArray`, etc.) inherit `Any`'s **referential** `equals`/`hashCode`: ```kotlin val a = intArrayOf(1, 2, 3) val b = intArrayOf(1, 2, 3) println(a == b) // false (identity) println(a.contentEquals(b)) // true (content) println(a.contentHashCode()) // content-based hash ``` Use `contentEquals`/`contentHashCode`, and `contentDeepEquals`/`contentDeepHashCode` for nested arrays. Putting a raw array in a `data class` gives you identity semantics in the generated `equals` — usually a bug; prefer `List` or override the members. ## 3. Mutable fields in `hashCode` ```kotlin data class Box(var v: Int) val s = hashSetOf(Box(1)) s.first().v = 99 // mutated a key println(s.contains(Box(99))) // often false — hash bucket no longer matches ``` Hash-based collections assume key hash codes are **stable**. Use immutable (`val`) properties for anything used as a key. ## 4. The override-both rule Override `equals` ⇒ override `hashCode`, and keep them consistent (`a == b` ⇒ same hash). Forgetting one silently breaks `HashMap`/`HashSet`. ## 5. `toString` is implicitly hot `toString` runs inside string templates (`"$obj"`), logging, debuggers, and exception messages. A `toString` that is expensive, recursive (cyclic object graphs → `StackOverflowError`), or throwing can cause far-reaching, hard-to-diagnose failures. ## Design guidance - Make value types **immutable**; derive `equals`/`hashCode`/`toString` from stable `val`s. - **Redact** sensitive fields in `toString`. - Use **content** array APIs or avoid raw arrays in value types. - Keep `toString` cheap and total (never throws).
- Why is `intArrayOf(1,2) == intArrayOf(1,2)` false?Arrays don't override `Any.equals`, so `==` falls back to referential identity. Two distinct array instances are never `==`; use `contentEquals` for value comparison.
- How do you keep a secret out of a data class's `toString`?Override `toString` to mask the field, or wrap the secret in a small type whose own `toString` returns a masked string so it can't leak even when embedded.
Auto-generated toString is like a name badge that prints your whole wallet contents — convenient until it shows your PIN.
saying these in an interview costs you the question
- Logging a data class with secret fields unredacted
- Using `==` to compare array contents
- Putting mutable `var` keys in a HashSet
- Raw array inside a value-semantics data class
- A `toString` that can throw or recurse infinitely