Kotlin generic <in, out, where> concepts and examples
In Kotlin, generics are used to specify the type of objects that a class, interface or method can operate on.
in
in
The keyword is used to specify that the generic type is an "input" type, meaning that it will only be used as an argument to a function or class.
interface ReadOnly {
fun read(): Any
}
class ReadWrite<in T>(private var value: T) : ReadOnly {
override fun read(): Any = value
// 'in' keyword allows to use T as an input only
// so, the following line will give a compile error
// fun write(value: T) { this.value = value }
}
Another example:
interface Consumer<in T> {
fun consume(item: T)
}
class StringConsumer : Consumer<String> {
override fun consume(item: String) {
println("Consuming string: $item")
}
}
class AnyConsumer : Consumer<Any> {
override fun consume(item: Any) {
println("Consuming any type: $item")
}
}
fun main() {
val stringConsumer = StringConsumer()
stringConsumer.consume("Hello") // prints "Consuming string: Hello"
val anyConsumer: Consumer<Any> = AnyConsumer()
anyConsumer.consume("Hello") // prints "Consuming any type: Hello"
anyConsumer.consume(123) // prints "Consuming any type: 123"
}
In the above example, Consumer
it is an interface with only one method consume
accepting a parameter of type T. The type parameter T is in
declared with a keyword indicating that it is only to be used as an input type. StringConsumer
and AnyConsumer
are two classes that implement Consumer
the interface, both of which can be used to consume instances of their respective types.
out
out
The keyword is used to specify that the generic type is an "output" type, meaning that it will only be used as the return type of a function or class.
interface WriteOnly {
fun write(value: Any)
}
class ReadWrite<out T>(private var value: T) : WriteOnly {
// 'out' keyword allows to use T as an output only
// so, the following line will give a compile error
// fun read(): T = value
override fun write(value: Any) {
// this.value = value
}
}
another example:
interface Producer<out T> {
fun produce(): T
}
class StringProducer : Producer<String> {
override fun produce(): String = "Hello"
}
class AnyProducer : Producer<Any> {
override fun produce(): Any = "Hello"
}
fun main() {
val stringProducer = StringProducer()
println(stringProducer.produce()) // prints "Hello"
val anyProducer: Producer<Any> = AnyProducer()
println(anyProducer.produce()) // prints "Hello"
}
In the above example, Producer
is an interface which has a single method produce
which returns a T
value of a type. A type parameter is declared T
with out
a keyword indicating that it is only to be used as an output type. StringProducer
and AnyProducer
are two classes that implement Producer
the interface, both of which can be used to generate instances of their respective types.
where
where
Keywords are used to specify constraints on types that can be used as parameters or return types.
interface Processor<T> where T : CharSequence, T : Comparable<T> {
fun process(value: T): Int
}
class StringProcessor : Processor<String> {
override fun process(value: String): Int = value.length
}
Another example:
interface Processor<T> where T : CharSequence, T : Comparable<T> {
fun process(value: T): Int
}
class StringProcessor : Processor<String> {
override fun process(value: String): Int = value.length
}
fun main() {
val stringProcessor = StringProcessor()
println(stringProcessor.process("Hello")) // prints "5"
}
In the above example, Processor
is an interface with just one method that process
takes T
a parameter of a type and returns one Int
. A type parameter is declared T
using where
the keyword and specifies two constraints: T must implement CharSequence
the interface, and it must be comparable to itself. StringProcessor
Is a class that implements a String
type's Processor
interface that can be used to manipulate String
values.
Consider the following class:
class Box<T>(val item: T)
This class defines a generic type T that can be used to specify the type of items stored in the Box. This class can be used to create any type of Box without any additional constraints:
val intBox = Box(1)
val stringBox = Box("hello")
Now consider the following class:
class InOut<in T, out R>(val item: T) {
fun get(): R {
return item as R
}
}
Here, T
is defined as an "input" type (using in
the keyword), R
and is defined as an "output" type (using out
the keyword). This means that it T
can only be used as an argument to a function, and R
only as a return type. This will allow us to define a InOut
function that takes a type and returns the inner item:
fun test(input: InOut<String, Any>): Any {
return input.get()
}
Finally consider the following class:
class MyClass<T> where T : Number, T : Comparable<T> {
fun compare(item1: T, item2: T): Int {
return item1.compareTo(item2)
}
}
Here, T
is defined as a generic type, limited to types that are both Number
and Comparable<T>
. This means that comparison functions can only be called with arguments of type Number
and .Comparable<T>
val myClass = MyClass<Int>()
val result = myClass.compare(1,2)
In this example, we can see that the class only accepts type Int since it is one Number
and is comparable.
By using in and out, Kotlin provides support for declaration-point variants, which allows us to define subtyping relationships for generic types at the point of declaration, rather than at the point of use. This allows us to use more generic types more safely and concisely, and prevents some possible type errors.
in conclusion
Here are some things you can do with in and out in Kotlin that would be difficult or impossible without them:
- Define covariant and contravariant generic types: out allows us to define covariant generic types, which means that subtype relationships are preserved (eg, subtypes of yes)
List<Child>
.List<Parent>
On the other hand, in allows us to define contravariant generic types, which means that the direction of the subtyping relationship is reversed (for example,Comparator<Parent>
isComparator<Child>
a subtype of ). - Using generic types in function parameters and return types: Using in and out allows us to use generic types in function parameters and return types to preserve subtyping relationships. For example, we can define a
List<out Parent>
function that takes as an argument, meaning it acceptsList<Child>
an orList<Parent>
, but notList<Grandparent>
. Similarly, we can define aComparator<in Child>
function that returns , which means it can returnComparator<Child>
orComparator<Parent>
, but notComparator<Grandparent>
. - Avoid casts and type checks: Using in and out can avoid casts and type checks in some cases because the compiler can infer subtyping relationships between different generic types. For example, if we have a
List<out Any>
, we can safely access the elements of the list as Any, since we know that all elements of the list are at least of type Any.
In summary, in and out are powerful tools in Kotlin, without them we would have to use casting, type checking and other workarounds to achieve the same level of expressiveness and safety.
reference
[Kotlin Generics] http://www.enmalvi.com/2021/01/31/kotlin-28/