Generic types in Kotlin provide a powerful way to create classes, interfaces, and functions that can operate on any type, while maintaining type safety. They allow you to write flexible and reusable code. Here’s a comprehensive guide to generic types in Kotlin:
1. Basics of Generics
Generics allow you to define a placeholder for a type, which can be specified when the generic class or function is instantiated or called.
Generic Classes
You define a generic class by specifying a type parameter in angle brackets <T>
:
class Box<T>(val value: T)
val intBox = Box(1) // Box<Int>
val stringBox = Box("Hello") // Box<String>val intBox = Box(1) // Box<Int>
val stringBox = Box("Hello") // Box<String>
Generic Functions
Functions can also be generic:
fun <T> box(value: T): Box<T> {
return Box(value)
}
val boxedInt = box(1)
val boxedString = box("Hello")
2. Type Constraints
Type constraints restrict the types that can be used as type arguments.
Upper Bounds
You can specify an upper bound using the :
syntax:
fun <T : Number> sum(a: T, b: T): Double {
return a.toDouble() + b.toDouble()
}
val result = sum(1, 2) // OK
val error = sum(1, "two") // Error: Type mismatch
Multiple Constraints
Multiple constraints can be specified using the where
clause:
fun <T> ensureNotNull(value: T) where T : Any, T : Comparable<T> {
// Implementation
}
3. Variance
Variance annotations control how subtypes of generic types are related to each other.
Covariance (out
)
Covariant types can be used as the return type, but not as the parameter type. This means that Producer<out T>
can produce T
but cannot consume T
.
class Producer<out T>(private val value: T) {
fun produce(): T = value
}
val anyProducer: Producer<Any> = Producer<String>("Hello")
Contravariance (in
)
Contravariant types can be used as the parameter type, but not as the return type. This means that Consumer<in T>
can consume T
but cannot produce T
.
class Consumer<in T> {
fun consume(value: T) {
// Implementation
}
}
val stringConsumer: Consumer<String> = Consumer<Any>()
Invariance
Without variance annotations, generic types are invariant, meaning Container<String>
is not a subtype of Container<Any>
, even if String
is a subtype of Any
.
class Container<T>(private val value: T) {
fun getValue(): T = value
fun setValue(newValue: T) {
value = newValue
}
}
// This would be an error:
// val anyContainer: Container<Any> = Container<String>("Hello")
4. Generic Constraints in Functions
Kotlin allows constraints directly in the function declaration:
fun <T> copyWhenGreater(list: List<T>, threshold: T): List<String>
where T : CharSequence, T : Comparable<T> {
return list.filter { it > threshold }.map { it.toString() }
}
Job Offers
5. Reified Type Parameters
Type parameters in inline functions can be marked as reified
, allowing you to access the type at runtime:
inline fun <reified T> isOfType(value: Any): Boolean {
return value is T
}
val result = isOfType<String>("Hello") // true
6. Star Projections
Star projections are a way to work with generic types when you don’t know the specific type parameter:
fun printList(list: List<*>) {
list.forEach { println(it) }
}
printList(listOf("Hello", "World"))
printList(listOf(1, 2, 3))
7. Generic Type Aliases
Type aliases can simplify complex generic types:
typealias StringMap<T> = Map<String, T>
val map: StringMap<Int> = mapOf("one" to 1, "two" to 2)
8. Advanced Example: Generic Repository
Here’s an advanced example of a generic repository:
interface Repository<T> {
fun getById(id: Int): T
fun save(item: T)
}
class UserRepository : Repository<User> {
override fun getById(id: Int): User {
// Implementation
}
override fun save(item: User) {
// Implementation
}
}
Conclusion
Generics in Kotlin are a powerful feature that enhances code reusability and type safety. By understanding and utilizing generic types, constraints, variance, and other related concepts, you can write more flexible and robust Kotlin code.
4o
This article is previously published on proandroiddev.com