skip to content

As a library author exposing a generic API consumed from both Kotlin and Java, when would you reach for `T & Any`, and what are the design and migration consequences?

level: principalimportance: nice to knowfreq 8%

answer

  1. Re-bound (T : Any) if nullable never valid; & Any if T must stay general
  2. & Any is the only option when overriding fixed external T
  3. Mirrors @NotNull/@Nullable exactly -> no platform types/!!
  4. Requires Kotlin 1.7+; tightening a position can break Kotlin callers
  5. No runtime cost; Java callers unaffected

basics

~20 s

Use T & Any when you must keep T flexible (often for Java overrides) but a specific input or output is always non-null. It documents the contract precisely without forcing all callers to drop nullable types.

solid answer

~50 s

For a cross-language generic API, prefer **re-bounding with `T : Any`** when nullable instantiations are simply invalid — it's enforced at every call site and is the clearest contract. Reach for `T & Any` when you cannot or should not re-bound: overriding/implementing a Java type whose `T` is fixed, or APIs where `T` must remain general (some positions nullable) yet a particular parameter/return is guaranteed non-null. Consequences: `T & Any` is a Kotlin-1.7+ feature, so consumers on older compilers may not understand it (a binary/source compatibility concern for libraries with old toolchains). It improves Kotlin call-site ergonomics (no platform types, no `!!`) and faithfully mirrors `@NotNull`/`@Nullable` Java contracts. From Java, the method still erases normally, so Java callers see no change. Migration risk: tightening a previously platform/nullable position to `T & Any` can become a source-incompatible change for Kotlin callers who relied on passing nulls. Treat it as a deliberate, documented contract narrowing.

code

kotlin · 11 lines
kotlin
// Faithfully mirror an annotated Java cache contract
interface Cache<K, V> {
    // both key and value definitely non-null, T stays general elsewhere
    fun getOrCompute(key: K & Any): V & Any
}

class StringCache : Cache<String, String> {
    private val m = mutableMapOf<String, String>()
    override fun getOrCompute(key: String & Any): String & Any =
        m.getOrPut(key) { key.uppercase() }
}

go deeper

for a junior

Can say it's used to mark a non-null position on a generic and helps Java interop.

for a middle

Chooses & Any vs T : Any correctly for a given API and writes the signature.

for a senior

Weighs interop fidelity, platform-type elimination, and basic compatibility impact.

for a principal

Frames the full decision tree (re-bound vs positional vs forced-by-inheritance), toolchain floor, source-compatibility/migration consequences, and Java-side erasure implications.

## The decision framework When designing a generic `<T>` API used from Kotlin and Java, ask three questions: 1. **Is a nullable `T` ever meaningful?** If never, declare `<T : Any>`. This is the strongest, simplest contract: callers cannot even write `Foo<String?>`. Use it when you own the API and control the bound. 2. **Must `T` stay general but one position is always non-null?** Then a bare bound won't do (it would forbid valid nullable instantiations elsewhere). Use `T & Any` **only at that position**. 3. **Are you implementing/overriding a fixed external `T`?** Then you cannot re-bound at all; `T & Any` is the only way to express a non-null position. This is the canonical Java-interop case. ## Why it matters for interop fidelity Java expresses nullability with annotations (`@NotNull`, `@Nullable`, JSpecify). `T & Any` lets a Kotlin declaration **exactly mirror** an annotated Java signature, so Kotlin consumers get precise types instead of platform types `T!`. That removes whole classes of latent `NullPointerException`s that platform types defer to runtime. ```kotlin // Mirrors Java: interface Cache<K, V> { @NotNull V getOrCompute(@NotNull K key); } interface Cache<K, V> { fun getOrCompute(key: K & Any): V & Any } ``` ## Compatibility and migration consequences - **Toolchain floor:** `T & Any` requires Kotlin 1.7+. A library targeting consumers on older compilers must weigh this. - **Source compatibility (Kotlin side):** narrowing a position from `T?`/platform to `T & Any` is a **breaking change** for Kotlin callers who passed nulls or relied on a nullable return. Stage it like any contract tightening (deprecation cycle, changelog). - **Java side:** generics erase identically; Java callers see no signature difference, but they also gain no static guarantee (Java ignores the Kotlin nullability). - **No runtime cost:** purely static; no extra checks or bytecode from the type itself. ## Alternatives and when to avoid `T & Any` - If you control the bound and nullability is meaningless: choose `T : Any` (clearer, restricts callers globally). - Overusing `T & Any` as a blanket 'safety' marker hurts readability; reserve it for genuine non-null positions on otherwise-general `T`. - Don't reach for unchecked casts / `@Suppress` — that's exactly what this feature replaces. ## Recap of concepts - upper bound `T : Any` (global) vs definitely-non-null `T & Any` (positional) - Java annotations `@NotNull`/`@Nullable`, JSpecify; platform types `T!` - JVM erasure; source vs binary compatibility; contract narrowing as a breaking change

  • Is changing a public API position from `T?` to `T & Any` a breaking change?
    For Kotlin callers, yes — those passing null or depending on a nullable type no longer compile. It's a deliberate contract narrowing and should go through a deprecation/changelog cycle. Java callers are unaffected.
  • When would you still prefer `T : Any` over `T & Any` in a public API?
    When nullable `T` is never meaningful for the whole abstraction and you control the bound. `T : Any` is enforced at every call site and is the clearest, simplest contract.

saying these in an interview costs you the question

  • Recommending `T & Any` everywhere instead of re-bounding when re-bounding is cleaner
  • Ignoring that narrowing to `T & Any` can break Kotlin source compatibility
  • Assuming Java callers gain a static non-null guarantee (they don't)
  • Forgetting the Kotlin 1.7 toolchain floor for older consumers
  • Falling back to unchecked casts / @Suppress instead of the feature

context