skip to content

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.

level: seniorimportance: nice to knowfreq 25%

answer

  1. <T : Any> when null is nonsensical
  2. Unbounded <T> keeps List<String?> possible
  3. Mark absence with T? per position
  4. Over-constrain = lose generality
  5. T & Any = definitely-non-null at a position

basics

~20 s

Use <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 s

Constrain <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
kotlin
// 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 unbounded

go deeper

for a junior

Knows <T : Any> blocks null and unbounded <T> allows it.

for a middle

Picks the bound based on whether null is meaningful and marks absence with T?.

for a senior

Articulates over- vs under-constraining trade-offs and where T & Any fits versus changing the bound.

for a principal

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

context