Kotlin, a modern programming language, provides powerful generic types that help in creating reusable and type-safe codes. Generics allow you to define classes, interfaces, and methods with placeholder types. These placeholders are then replaced by actual types when you instantiate or call these types. This article introduces the concept of generics in Kotlin and shows how to use them effectively.
Understanding Generics
Generics are a feature that allows a class, interface, or function to be more flexible by working with any data type. They serve as a template from which classes or methods can be instantiated.
Here's a simple example of a generic class:
// A simple generic class
class Box(t: T) {
var value = t
}
fun main() {
val intBox = Box(1) // Specifying T as Int
val stringBox = Box("Hello") // Specifying T as String
println(intBox.value) // Output: 1
println(stringBox.value) // Output: Hello
}
In this example, Box<T> is a generic class with a type parameter T. When we create instances of Box, we specify the type we want the class to use with <Int> or <String>.
Generic Functions
Kotlin also allows you to create generic functions. Here's an example:
// A function that can work with any type
fun printItem(item: T) {
println(item)
}
fun main() {
printItem(1) // Output: 1
printItem("Kotlin") // Output: Kotlin
printItem(12.34) // Output: 12.34
}
With the <T> before the function name, printItem is declared as a generic function. It can take a parameter of any type and print it.
Type Constraints
Sometimes you may want to restrict the types that can be used as parameters a little further by specifying bounds for the type parameter. You can achieve this using type constraints.
// A generic function with a type constraint
fun add(a: T, b: T): Double {
return a.toDouble() + b.toDouble()
}
fun main() {
println(add(5, 10)) // Output: 15.0
println(add(3.0, 7.0)) // Output: 10.0
// The following would cause a compile error, as String is not a subtype of Number
// println(add("Hello", "World"))
}
In this example, the generic type <T : Number> indicates that this function only accepts parameters of Number type or its subtypes. This provides more control over the types used in a generic function.
Variance: Invariance, Covariance, and Contravariance
Variance is an advanced concept in generics and defines how subtyping between more complex types relates to subtyping between their component types. Kotlin provides three keywords for generics variance:
- Invariant: Kotlin’s generic types are invariant by default, which means that generic types are invariant to subtyping changes unless declared otherwise.
- Covariant: Use the
outkeyword to make a generic type covariant. This means that you can substitute it with a subtype of that type. - Contravariant: Use the
inkeyword to make a generic type contravariant. This allows the utilization of the supertype property.
Example:
// An example of variance
class CoArray(private val values: Array<T>) {
fun get(index: Int): T {
return values[index]
}
// We cannot include this method as T is marked as 'out'
// fun set(value: T) {
// values[0] = value
// }
}
fun main() {
val stringArray = CoArray(arrayOf("Kotlin", "Java"))
val anyArray: CoArray<Any> = stringArray // This is allowed because we used 'out' keyword
println(anyArray.get(0)) // Output: Kotlin
}
The example demonstrates a class CoArray which is covariant. By using the out keyword, the class allows type substitution, meaning you can assign a CoArray<String> to a CoArray<Any>.
Conclusion
Generics are a powerful feature in Kotlin that increases the reusability of the code while keeping it type-safe. This article has given you an overview of how generics work and shown practical examples. Exploring generics more deeply, especially topics like variance or generic constructors, will further enable you to handle complex data structures with simplicity and robust type safety.