A generic class or function declares type parameters in angle brackets, same as Java's syntax on the surface. The real difference is variance: Kotlin supports DECLARATION-site variance, where the class itself marks a type parameter out (the type is only ever produced/returned, enabling safe covariance — a Producer<Cat> can be used where Producer<Animal> is expected) or in (only ever consumed/accepted, enabling contravariance). Java instead requires USE-site variance, repeating ? extends T or ? super T at every call site that needs it — Kotlin's approach means the variance is decided once, by the class author, and every caller benefits automatically without repeating any wildcard syntax.
A basic generic class
Box<T> works identically to a generic class in Java or C++ at this basic level — the interesting differences show up specifically around variance.
class Box<T>(private val item: T) {
fun get(): T = item
}
val intBox = Box(42)
val stringBox = Box("hello")out: declaration-site covariance
Marking T as out ONCE, in Producer's own declaration, means every user of Producer automatically gets safe covariance — a Producer<Cat> can be passed anywhere a Producer<Animal> is expected, with no wildcard syntax needed at any call site.
open class Animal
class Cat : Animal()
interface Producer<out T> { // out: T is only ever PRODUCED/returned, never consumed
fun produce(): T
}
fun printAnimal(producer: Producer<Animal>) {
println(producer.produce())
}
val catProducer: Producer<Cat> = object : Producer<Cat> {
override fun produce() = Cat()
}
printAnimal(catProducer) // fine — Producer<Cat> accepted where Producer<Animal> is expectedin: declaration-site contravariance
in is the mirror image — T is only ever consumed (accepted as a parameter), never produced, which safely allows the reverse substitution: a Consumer<Animal> can be used anywhere a Consumer<Cat> is expected.
interface Consumer<in T> { // in: T is only ever CONSUMED/accepted, never produced
fun consume(item: T)
}
val animalConsumer: Consumer<Animal> = object : Consumer<Animal> {
override fun consume(item: Animal) { println("Consumed an animal") }
}
val catConsumer: Consumer<Cat> = animalConsumer // fine — Consumer<Animal> works as Consumer<Cat>