kotlin generics <in, out, where> terms with examples 📝
In Kotlin, generics are used to specify the types of objects that a class, interface, or method can operate on.
IN
The in keyword is used to specify that a generic type is an "input" type, meaning it will only be used as an argument to a function or a class.
Another example:
In the above example, Consumer is an interface with a single method consume that takes an argument of type T. The type parameter T is declared with the in keyword, indicating that it is only used as an input type. StringConsumer and AnyConsumer are two classes that implement the Consumer interface, and both can be used to consume instances of their respective types.
Out
The out keyword is used to specify that a generic type is an "output" type, meaning it will only be used as a return type from a function or a class.
Another example:
In the above example, Producer is an interface with a single method produce that returns a value of type T. The type parameter T is declared with the out keyword, indicating that it is only used as an output type. StringProducer and AnyProducer are two classes that implement the Producer interface, and both can be used to produce instances of their respective types.
WHERE
The where keyword is used to specify constraints on the types that can be used as arguments or return types.
Another example:
In the above example, Processor is an interface with a single method process that takes an argument of type T and returns an Int. The type parameter T is declared with the where keyword, and two constraints are specified: T must implement the CharSequence interface, and it must be Comparable with itself. StringProcessor is a class that implements the Processor interface for String type, and it can be used to process String values.
For example, consider the following class:
This class defines a generic type T that can be used to specify the type of the item that is stored in the box. Without any additional constraints, this class can be used to create boxes of any type:
Now, let’s consider the following class:
Here, T is defined as an "input" type (using the in keyword) and R is defined as an "output" type (using the out keyword). This means that T can only be used as an argument to a function, and R can only be used as a return type. This would allow us to define a function that accepts a InOut type and returns the item inside:
Finally, let’s consider the following class:
Here, T is defined as a generic type that is restricted to types that are both Number and Comparable<T>. This means that the compare function can only be called with arguments of types that are both Number and Comparable<T>.
In this example, we can see that the class only accepts Int type because it is a Number and it is comparable.
With the use of in and out, Kotlin provides support for declaration-site variance, which allows us to define how subtyping works for generic types at the declaration site, rather than at the use site. This enables us to use more generic types safely and concisely, and it prevents some kinds of type errors that would be possible without it.
Here are a few things that we 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 subtyping is preserved (i.e., a List<Child> is a subtype of List<Parent>). On the other hand, in allows us to define contravariant generic types, which means that the direction of subtyping is reversed (i.e., a Comparator<Parent> is a subtype of Comparator<Child>).
- Use generic types in function parameters and return types: Using in and out allows us to use generic types in function parameters and return types in a way that preserves subtyping. For example, we can define a function that takes a List<out Parent> as a parameter, which means that it can accept a List<Child> or a List<Parent>, but not a List<Grandparent>. Similarly, we can define a function that returns a Comparator<in Child>, which means that it can return a Comparator<Child> or a Comparator<Parent>, but not a Comparator<Grandparent>.
- Avoid casting and type checks: Using in and out allows us to avoid casting and type checks in some cases, because the compiler can infer the subtyping relationships between different generic types. For example, if we have a List<out Any>, we can safely access elements of the list as Any, because we know that all the 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 resort to casting, type checks, and other workarounds to achieve the same level of expressiveness and safety.