When designing a generic API, when should you constrain <T : Any> versus accepting an unbounded <T> and using T? at specific positions? Discuss the trade-offs and a case where each is the right call.
answer
- <T : Any> when null is nonsensical
- Unbounded <T> keeps List<String?> possible
- Mark absence with T? per position
- Over-constrain = lose generality
- T & Any = definitely-non-null at a position
basics
~20 sUse <T : Any> when nulls make no sense for your type and you want callers blocked from passing them. Use unbounded <T> when nullable element types are legitimate, and mark only the positions that can be null with T?.
solid answer
~40 sConstrain <T : Any> when your abstraction is meaningless for null — e.g., a non-null registry, a builder requiring real values, or keys that must exist. The payoff: callers can't pass null, and inside the body T is non-null so no defensive checks are needed. Accept an unbounded <T> (Any?) when storing nullable elements is valid — general-purpose containers like List<T> deliberately allow List<String?> — and then use T? only where absence is meaningful (return types of lookups, optional fields). Over-constraining with <T : Any> reduces a container's generality; under-constraining forces every member to handle nullable T defensively. A nuance: an unbounded <T> still lets you express non-null-at-a-position needs with Kotlin's definitely-non-null type T & Any when overriding nullable-accepting Java APIs, but for fresh Kotlin APIs the choice is mainly bound-vs-T?.
code
kotlin · 12 lines// Non-null abstraction: constrain the bound
class Builder<T : Any> {
private val parts = mutableListOf<T>()
fun add(part: T) = apply { parts += part } // null blocked
}
// General container: stay unbounded, null only where meaningful
interface Cache<K, V> {
fun put(key: K, value: V)
fun get(key: K): V? // miss => null
}
val c: List<String?> = listOf(null) // possible only because List is unboundedgo deeper
Knows <T : Any> blocks null and unbounded <T> allows it.
Picks the bound based on whether null is meaningful and marks absence with T?.
Articulates over- vs under-constraining trade-offs and where T & Any fits versus changing the bound.
Sets API-design guidance balancing generality, contract honesty, and interop for a whole library or platform.
## The decision Two dials control nullability in a generic API: 1. **The bound** — `<T : Any>` (non-null) vs unbounded `<T>` (== `<T : Any?>`, nullable allowed). 2. **Per-position nullability** — using `T` vs `T?` on individual parameters/returns. ## When `<T : Any>` is right Choose it when **null is nonsensical** for the abstraction: ```kotlin class NonNullRegistry<T : Any> { private val items = mutableListOf<T>() fun add(item: T) { items += item } // null genuinely invalid } ``` Benefits: - Callers are **blocked** from `add(null)` at compile time. - Inside the body `T` is non-null, so **no defensive null handling**. - The intent ('these are real values') is documented in the signature. Good fits: builders, required-key maps, identity registries, math over non-null elements. ## When unbounded `<T>` is right Choose it when **nullable elements are legitimate**. General-purpose containers do this on purpose: ```kotlin val xs: List<String?> = listOf("a", null) // valid and useful ``` If `List` were `<T : Any>` you could never have a `List<String?>`. So you keep `T` unbounded and add `T?` only where *absence* is meaningful: ```kotlin interface Lookup<K, V> { fun get(key: K): V? // miss => null, regardless of V's nullability } ``` ## The trade-off, stated plainly - **Over-constraining** (`<T : Any>` when you didn't need to) loses generality — no nullable element types. - **Under-constraining** (unbounded when null is meaningless) pushes null handling into every member and weakens the contract. ## A nuance: definitely-non-null types With an unbounded `<T>` you can still demand non-null at one position using Kotlin's **definitely-non-null** type `T & Any` — handy when overriding a Java method whose signature is `@Nullable`-flexible but you want to return non-null. For brand-new Kotlin-only APIs, the everyday choice is simply bound vs `T?`; `T & Any` is the tool when the bound can't be changed but a position must be non-null. ## Rule of thumb - Default to the **least restrictive** bound that keeps the contract honest. - Add `<T : Any>` when null is truly invalid for the whole type. - Use `T?` to mark the few positions where absence is real.
- Why isn't List declared as List<T : Any>?Because List<String?> is a legitimate, useful type; constraining to Any would forbid nullable element collections.
- When would you reach for T & Any instead of <T : Any>?When the bound is fixed (e.g., overriding a platform-flexible Java method) but one specific position must be non-null.
The bound is the building's front-door policy; T? is a 'may be empty' sign on individual rooms. Lock the door only if no empty room should ever exist.
saying these in an interview costs you the question
- Always constraining <T : Any> 'to be safe', losing generality
- Using unbounded <T> then sprinkling !! everywhere
- Thinking List should be <T : Any>
- Confusing T? with the definitely-non-null T & Any
- Not tying the bound choice to whether null is meaningful for the type