Kotlin
2026-08-21 09:00
List<String> · (Int) -> Int · Map<String, Person>
This unit explains them.
Declaring — mostly syntax you know from Java.
Erasure — a JVM constraint, with a clever workaround.
Variance — a genuine question, and the hard part.
If String is a subtype of Any,
is List<String> a subtype of List<Any>?
The answer is sometimes.
Without the bound, > would not compile.
An unbounded T has upper bound Any?. So T is nullable.
Unit 6’s discipline, applied to type parameters.
The permissive case is the default. Always.
Generics were added to Java after the fact.
For compatibility, the type arguments are thrown away.
Kotlin inherits this, because it targets the JVM.
Elements read out are Any?. You cannot write.
List<Any?> — the element type is Any?.
List<*> — the element type is something specific we do not know.
Confusing them produces surprising errors, usually a refused write.
Almost always.
The compiler already checked; there is nothing left to verify.
It bites in exactly two places: runtime type checks and reflection.
Inlining substitutes the body at the call site.
And the type argument is known at each call site.
isA<String>(x) becomes, literally, x is String.
Only inline functions can have reified parameters.
A non-inlined function has one body shared by all callers.
No single type to substitute.
Unit 8: non-local returns.
Unit 9: reified type parameters.
The reflection unit is full of the second shape.
Reification requires inlining.
Keep reified functions small.
Extract the bulk into a non-inline helper.
A String may be used where an Any is expected.
May a List<String> be used where a List<Any> is expected?
Only produces → safe. Everything out is a String, and a String is an Any.
Also consumes → not safe.
List only produces → can be covariant.
MutableList consumes → cannot.
That earlier split exists to make this one possible.
Covariant — subtyping preserved.
Contravariant — subtyping reversed.
Invariant — neither. Java’s default, which is why wildcards exist.
Only produces. Subtyping preserved.
Position rule: may appear only in return types.
Only consumes. Subtyping reversed.
A Comparator<Any> can compare two strings.
outfor producers,infor consumers.
And the position rules follow from it:
a producer’s type is where values come out; a consumer’s, where they go in.
A function accepting more kinds of input
and returning a more specific result
can stand in for one that accepts fewer and returns something vaguer.
Java: at every use. Kotlin: once, on the class.
source.add(...) will not compile. The projection is enforced.
Most of the time you use variance without declaring any.
List is already out T. Comparator is already in T.
Does this type produce its parameter, consume it, or both?
Produce → out. Consume → in.
Both → invariant, and project at the use sites that need one direction.
T is nullable — its upper bound is Any?.List<*> and List<Any?> are different claims.reified works because the type argument is known at each call site.out for producers, in for consumers — declared once, unlike Java’s wildcards.Annotations and Reflection — inspecting and acting on code at runtime.
Erasure returns immediately, since reflection is where it is most missed.
And reified parameters return as what makes reflection APIs pleasant to call.
Kotlin · Generics