Working with Kotlin Generics
1. Creating Generic Classes
Generic classes in Kotlin allow you to define classes that work with different types while maintaining type safety at compile time.
| Feature | Description | Example |
|---|---|---|
| Generic Class Declaration | Use type parameters in angle brackets after the class name. | class Box<T>(value: T) { ... } |
| Type Parameter Constraints | Use : T to constrain type parameters to a specific supertype. |
class Container<T : CharSequence>(value: T) { ... } |
| Reified Type Parameters | Use reified to access type information at runtime (only in inline functions). |
inline fun <reified T> getType(): Class<T> = T::class.java |
Example: Basic Generic Class
class Box<T>(private val value: T) {
fun getValue(): T = value
}
val intBox = Box(42)
val stringBox = Box("Hello")
Example: Generic Class with Constraints
class Container<T : CharSequence>(private val value: T) {
fun getValue(): T = value
fun length(): Int = value.length
}
val container = Container("Kotlin")
2. Implementing Generic Functions
Generic functions allow you to write flexible, reusable code that works with different types. They are declared using a type parameter in angle brackets.
| Function | Description | Example |
|---|---|---|
| fun <T> identity(value: T): T | Generic function that returns the same type as input | |
| fun <T> processList(list: List<T>, processor: (T) -> T): List<T> | Generic function that processes a list of any type | |
<String>.Any as a type parameter unless necessary, as it reduces type safety.3. Using Variance
| Concept | Description | Example |
|---|---|---|
| Covariance | Allows a generic type to be used with a more derived type than originally specified. | |
| Contravariance | Allows a generic type to be used with a less derived type than originally specified. | |
| Invariance | Generic type is invariant, meaning it must match exactly. | |
Example: Covariance in Collections
val strings = listOf("a", "b", "c")
val anyList: List<Any> = strings // Covariance allows this
Example: Contravariance in Function Types
fun processString(s: String) { }
fun processAny(a: Any) { }
val stringProcessor: (String) -> Unit = ::processString
val anyProcessor: (Any) -> Unit = ::processAny
// This works because function types are contravariant in their parameter
val func: (Any) -> Unit = stringProcessor // Valid due to contravariance
4. Implementing Covariance
Covariance in Kotlin allows a generic type to be used with a more derived type than originally specified. It's particularly useful when dealing with collections and functions.
| Feature | Description | Example |
|---|---|---|
| out | Specifies that a type parameter is covariant. It can only be used in output positions (e.g., return types). | |
| in | Specifies that a type parameter is contravariant. It can only be used in input positions (e.g., function parameters). | |
| invariant | Default behavior where the type parameter is neither covariant nor contravariant. It must match exactly. | |
Example: Covariant Producer
open class Animal {
open fun makeSound() = "Some sound"
}
class Dog : Animal() {
override fun makeSound() = "Bark"
}
interface Producer<out T> {
fun produce(): T
}
class AnimalProducer : Producer<Animal> {
override fun produce(): Animal = Animal()
}
class DogProducer : Producer<Dog> {
override fun produce(): Dog = Dog()
}
fun useAnimalProducer(p: Producer<Animal>) {
val animal = p.produce()
println(animal.makeSound())
}
fun main() {
val dogProducer: Producer<Dog> = DogProducer()
// Covariance allows this assignment
val animalProducer: Producer<Animal> = dogProducer
useAnimalProducer(animalProducer)
}
Example: Contravariant Consumer
open class Animal {
open fun makeSound() = "Some sound"
}
class Dog : Animal() {
override fun makeSound() = "Bark"
}
interface Consumer<in T> {
fun consume(item: T)
}
class AnimalConsumer : Consumer<Animal> {
override fun consume(item: Animal) {
println("Consuming animal: ${item.makeSound()}")
}
}
class DogConsumer : Consumer<Dog> {
override fun consume(item: Dog) {
println("Consuming dog: ${item.makeSound()}")
}
}
fun useAnimalConsumer(c: Consumer<Animal>) {
c.consume(Animal())
}
fun main() {
val dogConsumer: Consumer<Dog> = DogConsumer()
// Contravariance allows this assignment
val animalConsumer: Consumer<Animal> = dogConsumer
useAnimalConsumer(animalConsumer)
}
5. Implementing Contravariance
| Type Parameter | Description | Usage Example |
|---|---|---|
| in T | Contravariant type parameter; can only be used as input (parameter) | fun processFile(writer: Writer<in String>) { ... } |
| out T | Covariant type parameter; can only be used as output (return value) | fun readData(): Reader<out String> { ... } |
Example: Using Contravariance in Function Parameters
interface Consumer<in T> {
fun accept(t: T)
}
class StringConsumer : Consumer<String> {
override fun accept(t: String) {
println("Received: $t")
}
}
class AnyConsumer : Consumer<Any> {
override fun accept(t: Any) {
println("Received: $t")
}
}
fun useConsumer(c: Consumer<Any>) {
c.accept("Hello")
}
// This works because String is a subtype of Any
val stringConsumer = StringConsumer()
useConsumer(stringConsumer)
Example: Contravariance in Collections
open class Animal {
fun feed() { println("Feeding animal") }
}
class Dog : Animal() {
override fun feed() {
println("Feeding dog")
}
}
class AnimalShelter<in T : Animal> {
fun addAnimal(animal: T) {
println("Adding animal to shelter")
}
}
val dogShelter = AnimalShelter<Dog>()
val animalShelter = AnimalShelter<Animal>()
// This works because Dog is a subtype of Animal
dogShelter.addAnimal(Dog())
animalShelter.addAnimal(Dog()) // Valid due to contravariance
in keyword and ensures that a type parameter can only be used in input positions, which allows for more flexible subtyping relationships.
6. Using Type Projections (star projection *)
Type projections in Kotlin allow you to use a generic type with a wildcard-like syntax. The star projection is denoted by * and is used when you want to express that a generic type can be of any type.
| Usage | Description | Example |
|---|---|---|
| out T | Produces values of type T | List<out T> |
| in T | Consumes values of type T | Consumer<in T> |
| T | Unbounded type parameter | List<*> |
Example: Star Projection in Lists
fun printListItems(list: List<*>) {
for (item in list) {
println(item)
}
}
val stringList = listOf("Hello", "World")
printListItems(stringList) // Works with any list type
Example: Star Projection in Function Parameters
fun processItems(items: MutableList<out String>) {
// This function can only read from the list
for (item in items) {
println(item)
}
}
val mutableList = mutableListOf("A", "B", "C")
processItems(mutableList)
7. Applying Generic Constraints
| Constraint Type | Description | Example |
|---|---|---|
| Upper Bound | Restricts type to be a subtype of a specified class | |
| Lower Bound | Restricts type to be a supertype of a specified class | |
| Multiple Bounds | Combines multiple constraints using & operator | |
| Reified Types with Constraints | Allows runtime type checks using reified generics | |
Example: Using Upper Bound Constraint
fun <T : Number> calculateAverage(numbers: List<T>): Double {
return numbers.map { it.toDouble() }.average()
}
val intList = listOf(1, 2, 3, 4, 5)
val doubleList = listOf(1.1, 2.2, 3.3)
println(calculateAverage(intList)) // Output: 3.0
println(calculateAverage(doubleList)) // Output: 2.2
8. Understanding Type Erasure
| Concept | Description | Implication |
|---|---|---|
| Type Erasure | Generic type information is removed at runtime in Java and Kotlin | Reflection and runtime type checks are limited |
Example: Runtime Type Check Limitation
fun<T> printType(obj: T) {
// This will not work as expected due to type erasure
if (obj is List<String>) {
println("List of strings")
}
}
Key Points
- Type erasure removes generic type information at runtime
- Runtime checks using
isoperator cannot distinguish between generic types - Workarounds include using
KClassorreifiedfunctions
9. Using Reified Type Parameters
In Kotlin, reified type parameters allow you to access the runtime type information of a generic type within a function. This is particularly useful when working with inline functions and reflection, where you need to know the actual type at runtime.
| Feature | Description | Usage |
|---|---|---|
| reified | Marks a type parameter as reifiable, allowing runtime access to its type. | inline fun <reified T> foo() { ... } |
| T::class | Accesses the runtime KClass of the reified type parameter. |
val clazz = T::class |
Example: Using Reified Type Parameters
inline fun <reified T> printType() {
println("Type: ${T::class.simpleName}")
}
fun main() {
printType<String>()
printType<Int>()
}
reified keyword is only allowed in inline functions because it requires the compiler to inline the function and replace the type parameter with its actual type at compile time.10. Working with Generic Extensions
| Extension | Description | Example |
|---|---|---|
| Generic Extension Functions | Allow adding functions to existing classes without inheritance or composition. | |
| Generic Extension Properties | Enable adding properties to existing classes using generics. | |
Example: Safe List Access
val numbers = listOf(1, 2, 3)
val result = numbers.safeGet(5) // Returns null
val validResult = numbers.safeGet(1) // Returns 2