Generics are a powerful feature of Kotlin that allow you to write flexible and reusable code by parameterizing types. They enable you to create classes, interfaces, and functions that operate on objects of various types while providing type safety at compile time. This tutorial will explore the fundamentals of generics in Kotlin, including their syntax, use cases, and best practices.
In Kotlin, generics are defined using angle brackets (<>). You can declare a generic class or function by specifying one or more type parameters within these brackets.
class Box<T>(var content: T) {
fun getContent(): T = content
}
In this example, Box is a generic class with a single type parameter T. The variable content holds an object of type T, and the method getContent() returns that object.
fun <T> printElement(element: T) {
println("Element: $element")
}
Here, printElement is a generic function that takes a parameter of type T and prints it. The type parameter <T> is specified before the function name.
Type constraints allow you to restrict the types that can be used as type arguments for a generic class or function. You can specify an upper bound using the : operator.
class Container<T : Number>(var value: T) {
fun getValue(): Double = value.toDouble()
}
In this example, Container is a generic class that only accepts types that are subclasses of Number. The method getValue() converts the stored value to a Double.
You can also specify multiple type constraints using the where keyword.
fun <T> copyWhenGreater(list: List<T>, threshold: T): List<T>
where T : Number, T : Comparable<T> {
return list.filter { it > threshold }
}
Here, copyWhenGreater is a generic function that filters elements in the list greater than the given threshold. It requires T to be both a Number and Comparable.
By default, generics in Kotlin are invariant, meaning you cannot assign a List<SubClass> to a variable of type List<SuperClass>. This ensures type safety but can sometimes be restrictive.
open class Animal
class Dog : Animal()
fun processAnimals(animals: List<Animal>) {
// Process animals
}
val dogs: List<Dog> = listOf(Dog())
// processAnimals(dogs) // Error: Type mismatch
Covariance allows a generic type to be used in a more general context. In Kotlin, you can achieve covariance using the out keyword.
fun <T> feedAnimals(animals: List<out Animal>) {
for (animal in animals) {
println("Feeding ${animal::class.simpleName}")
}
}
val dogs: List<Dog> = listOf(Dog())
feedAnimals(dogs) // Valid
In this example, List<out Animal> is a covariant list of Animal or its subclasses. The function feedAnimals can accept any list of animals.
Contravariance allows a generic type to be used in a more specific context. In Kotlin, you can achieve contravariance using the in keyword.
fun <T> addDogs(dogList: MutableList<in Dog>, dog: T) where T : Dog {
dogList.add(dog)
}
val animals: MutableList<Animal> = mutableListOf(Animal())
addDogs(animals, Dog()) // Valid
Here, MutableList<in Dog> is a contravariant mutable list of Dog or its superclasses. The function addDogs can add any subclass of Dog to the list.
Reified types allow you to access the type information at runtime within inline functions. This feature is particularly useful for generic functions that need to perform operations based on the actual type of the argument.
inline fun <reified T> createInstance(): T {
return when (T::class) {
String::class -> "Hello" as T
Int::class -> 42 as T
else -> throw IllegalArgumentException("Unsupported type")
}
}
val str: String = createInstance()
val num: Int = createInstance()
In this example, createInstance is an inline function with a reified type parameter. It creates and returns an instance of the specified type.
Generics are a powerful feature in Kotlin that enhance code flexibility and safety by allowing you to parameterize types. By understanding their syntax, constraints, variance, and reified types, you can write more robust and reusable code. This tutorial has provided a comprehensive guide to generics in Kotlin, covering both basic and advanced concepts.