Generics means we use a class or an implementation in a very generic manner. For example the interface List allows us for code reuse. We are able to create a list of Strings, of integer values and we will have the same operations even if we have different types. So the list wraps a common functionality for each implementation.
Kotlin allows you to use parameters for methods and attributes, composing what is known as parameterized classes.
1️⃣Type vs Class vs Subtype
- Type describes the properties that a set of objects may share.
- Class is just an implementation of that type.
- A subtype must accept at least the same range of types as its supertype declares.
- A subtype must return at most the same range of types as its supertype declares.
Variance refers to how subtyping between more complex types relates to subtyping between their components.
Conventions: E = element | T = type | K = key | V = value
- If C<T> is a generic type with type parameter T and U is a subtype of T, then C<U> is a subtype of C<T>
- Example: List<Int> is a subtype of List<Number> because Int is a subtype of Number.
- Applies to types that are “producers”, or a “source” of T
- T only appears only in “out” position, i.e., the return type of a function
- If C<T> is a generic type with type parameter T and U is a subtype of T, then C<T> is a subtype of C<U>
- U subtype of T ⇒ C<T> subtype of C<U>
- Example: Function1<Number, Int> is a subtype of Function1<Int, Int> because Int is a subtype of Number.
- Applies to types that are “consumers” of T
- T only appears only in “in” position, i.e., the type of a function argument
- If C<T> is a subtype of C<U>, then T = U
- Example: Array<T> is invariant in T
- T appears in both “in position” and “out position”
- Type is both a producer and consumer of T
- To remember: Lambdas are contra-variant in their argument types and covariant in their return type
- A type projection is a type that has been limited in certain ways in order to gain variance characteristics using use-site variance.
- They come in handy when we know nothing about the type argument, but need to use them in a safe way.
- The safe way here is to define such a projection of the generic type, that every concrete instantiation of that generic type would be a subtype of that projection.
- Instead of using C<in Nothing> or C<out Any?>, you can just use C<*>. It produces the same effective interface as the other two approaches:
- So then, we have three ways that we can accept any kind of a generic:
- in-projection – <in Nothing>
- out-projection – <out Any?>
- star-projection – <*>
8️⃣Type erasure and reified type parameters
- Java has limits on what types are considered reifiable – meaning they’re “completely available at run time” (see the Java SE specs on reifiable types)
- The type safety checks that Kotlin performs for generic declaration usages are only done at compile time. At runtime, the instances of generic types do not hold any information about their actual type arguments.
- Enforcing type constraints only at compile time and discarding the element type information at runtime.
- The type information is said to be erased.
- In the case of reified type parameters in Kotlin we can compare types and get Class objects.
- Reified types parameters:
- Work only with functions (or extension properties that have a get() function)
- Work with those functions that are declared to be inline
- Advantages of using reified types parameters:
- type checks with is
- casts without unchecked cast warnings
- assign class objects by appending ::class.java to the parameter name. For example: val a = T::class.java
📚 Learn more
- Reified type parameters https://kotlinlang.org/docs/reference/inline-functions.html#reified-type-parameters
- Generics https://kotlinlang.org/docs/reference/generics.html
Enjoy and feel free to leave a comment if something is not clear or if you have questions. And if you like it please share!
Thank you for reading!