Kotlin Generics - in, out, where
In Kotlin, generics provide the ability to create classes, interfaces, and methods that can work with various types, without specifying the actual type until the object is created.
In
The <in>
keyword in generics is used to specify a type parameter as a contravariant. Contravariant means that the type can be used as input parameters.
Here’s an example of using <in>
with generics in Kotlin:
interface AnimalCare<in T> {
fun takeCareOf(animal: T)
}
class Vet : AnimalCare<Animal> {
override fun takeCareOf(animal: Animal) {
// do something
}
}
class Animal {}
class Dog : Animal {}
fun main() {
val animalCare: AnimalCare<Dog> = Vet()
animalCare.takeCareOf(Dog())
}
In the above example, AnimalCare
is a generic interface with the type parameter T
marked as in
. The Vet
class implements the AnimalCare
interface with the type parameter Animal
. The main
function creates an instance of AnimalCare
with the type parameter Dog
, but assigns it to a variable of type AnimalCare<Animal>
. Since Dog
is a subtype of Animal
, it can be passed to the takeCareOf
method of the Vet
class.
Using the <in>
keyword in generics allows for a greater degree of flexibility in using classes, interfaces, and methods with different types. It is especially useful when dealing with classes that accept input parameters, such as with the takeCareOf
method in the above example.
Out
In Kotlin, the <out>
keyword is used to define covariance in generic classes and interfaces. It allows for a more flexible use of generics, allowing a subtype of a generic type to be used in place of the parent type.
When a generic class or interface is defined with the <out>
keyword, it means that the type parameter can only appear in the output positions of member functions. This means that it can only be returned from functions or used as a property type.
Here’s an example of a generic interface using the <out>
keyword:
interface Producer<out T> {
fun produce(): T
}
In this example, T
is a covariant type parameter. This interface has a single function produce()
that returns an object of type T
. Since T
is covariant, the produce()
function can return a subtype of T
.
Here’s an example of how this interface can be used:
class FruitProducer : Producer<Fruit> {
override fun produce(): Fruit {
return Apple()
}
}
In this example, Fruit
is the type parameter for Producer
. The FruitProducer
class implements the Producer
interface and overrides the produce()
function to return an object of type Fruit
, which in this case is an instance of Apple
. Since T
is covariant, FruitProducer
is allowed to return an Apple
object instead of a Fruit
object.
Using the <out>
keyword in generics can make your code more flexible and reusable. It allows you to use subtypes of a generic type where the parent type is expected, making your code more adaptable to changes in the future.
Where
The where
keyword in Kotlin is used to add constraints on the types used as generic type parameters. It allows you to specify that a generic type parameter must be a subtype of a specific class or implement a specific interface.
Here’s an example of using where
to add constraints on a generic function:
fun <T> addNumbers(a: T, b: T) where T : Number {
val result = a.toDouble() + b.toDouble()
println("Result: $result")
}
In the above example, we’ve added a constraint on the generic type parameter T
using the where
keyword. The constraint specifies that T
must be a subtype of the Number
class.
Now, let’s see how we can use this function:
addNumbers(3, 5) // Output: Result: 8.0
addNumbers(2.5, 4.5) // Output: Result: 7.0
addNumbers("hello", "world") // Compilation error: Type parameter bound for 'T' is not satisfied
As you can see, the function can only be called with arguments that are subtypes of Number
.
You can also use the where
keyword to add multiple constraints on a generic type parameter. Here's an example:
fun <T> processList(list: List<T>) where T : CharSequence, T : Comparable<T> {
val sortedList = list.sorted()
sortedList.forEach { println(it) }
}
In the above example, we’ve added two constraints on the generic type parameter T
using the where
keyword. The constraints specify that T
must be a subtype of the CharSequence
class and must also implement the Comparable
interface.
Now, let’s see how we can use this function:
val stringList = listOf("apple", "banana", "orange")
processList(stringList) // Output: apple banana orange
val intList = listOf(3, 2, 1)
processList(intList) // Compilation error: Type parameter bound for 'T' is not satisfied
As you can see, the function can only be called with arguments that are subtypes of CharSequence
and implement the Comparable
interface.
In summary, the where
keyword in Kotlin allows you to add constraints on generic type parameters to restrict the types that can be used as arguments for generic functions or classes.