Generics are a fundamental concept in Kotlin that helps make code more flexible, reusable, and type-safe. If you’ve ever wondered how Kotlin allows functions and classes to operate on different data types while maintaining type safety, you’re in the right place. In this article, we’ll explore Generic Type Parameters in Kotlin in a simple and approachable way.
What Are Generic Type Parameters?
Generic type parameters allow you to write code that can work with different types while enforcing compile-time type safety. Instead of specifying a fixed type, you define a placeholder (like T
), which can represent any type the user provides.
For example, without generics, you’d need multiple implementations of the same function for different types:
fun printInt(value: Int) {
println(value)
}
fun printString(value: String) {
println(value)
}
With generics, you can write a single function:
fun <T> printValue(value: T) {
println(value)
}
Now, printValue
can accept any type while still ensuring type safety!
Declaring Generic Classes
Generics shine when defining reusable classes. Let’s take a simple example of a generic class:
class Box<T>(private val item: T) {
fun getItem(): T {
return item
}
}
Here, T
is a type parameter that can be replaced with any type at the time of object creation:
val intBox = Box(10)
val stringBox = Box("Hello, Kotlin!")
println(intBox.getItem()) // Output: 10
println(stringBox.getItem()) // Output: Hello, Kotlin!
This approach makes our Box
class more versatile and eliminates the need for multiple implementations.
Bounded Type Parameters
Sometimes, you may want to restrict the type that can be used as a generic parameter. Kotlin allows you to specify upper bounds using :
, ensuring that only subtypes of a specified class/interface can be used.
For example, let’s create a function that works only with numbers:
fun <T : Number> doubleValue(value: T): Double {
return value.toDouble() * 2
}
Now, calling doubleValue
with an Int
, Double
, or Float
works, but passing a String
results in a compilation error:
println(doubleValue(5)) // Output: 10.0
println(doubleValue(3.5)) // Output: 7.0
// println(doubleValue("Hello")) // Compilation Error!
This ensures that our function is used correctly while retaining the benefits of generics.
Variance in Kotlin Generics
Variance determines how generics behave with subtype relationships. Kotlin provides two keywords: out
and in
to handle variance properly.
Covariance (out
)
When you declare out T
, it means the type parameter is only produced (returned) and not consumed (accepted as a function parameter). This is useful for read-only data structures like List<T>
.
interface Producer<out T> {
fun produce(): T
}
Since T
is only returned, we can safely use out
to allow subtype assignment:
val stringProducer: Producer<String> = object : Producer<String> {
override fun produce() = "Hello"
}
val anyProducer: Producer<Any> = stringProducer // Allowed because of 'out'
println(anyProducer.produce()) // O/P - Hello
Contravariance (in
)
Conversely, in T
means the type parameter is only consumed (accepted as a function parameter) and never produced. This is useful for function parameters.
interface Consumer<in T> {
fun consume(item: T)
}
Since T
is only an input, we can safely use in
to allow supertype assignment:
val anyConsumer: Consumer<Any> = object : Consumer<Any> {
override fun consume(item: Any) {
println("Consumed: $item")
}
}
val stringConsumer: Consumer<String> = anyConsumer // Allowed because of 'in'
println(stringConsumer.consume("Hello")) // O/P - Consumed: Hello
Understanding variance prevents type mismatches and allows for better design patterns when working with generics.
Reified Type Parameters in Inline Functions
Kotlin has a limitation where type parameters are erased at runtime (type erasure). However, using reified type parameters in inline functions allows you to access the actual type at runtime.
inline fun <reified T> isTypeMatch(value: Any): Boolean {
return value is T
}
println(isTypeMatch<String>("Kotlin")) // Output: true
println(isTypeMatch<Int>("Kotlin")) // Output: false
This is especially useful when working with reflection, type-checking, or factory methods.
Conclusion
Understanding Generic Type Parameters in Kotlin is key to writing flexible and type-safe code. By using generics, you can create reusable functions and classes while ensuring compile-time safety. Whether it’s defining generic classes, enforcing type constraints, or handling variance, generics make Kotlin a powerful and expressive language.