Generics means that we use a class or an implementation in a very general way. For example, the List interface allows us to reuse code. We can create a String list, an integer list, and we will have the same operation even when the types are different. So the list is like a common function for every implementation.
Kotlin allows you to use parameters for methods and attributes, creating parameterized classes.
1. Type vs Class vs Subtype
Type
describes the properties that a collection of objects can share.Class
is just an implementation of that Type.- A
subtype
must accept at least the same types of types as its supertype declaration. - A
subtype
must return at most the same types of types as its supertype declaration.
2. Variance
Variance says how subtyping between types is more complicated with regards to subtyping between their components.
The convention: E = element | T = type | K = key | V = value
Code Sample:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | data class Course(val name: String) class OddList<T>(val list: List<T>) { fun oddItems(): List<T> { return list.filterIndexed { index, _ -> index % 2 == 1 } } } fun main() { val listOfStrings = listOf("Kotlin", "Java", "C#") val resultOfStrings: OddList<String> = OddList(listOfStrings) println(resultOfStrings.oddItems()) val listOfInts = listOf(1, 7, 8, 9, 12, 45) val resultOfInts = OddList(listOfInts) println(resultOfInts.oddItems()) val courses = listOf( Course("Kotlin"), Course("Java"), Course("C#"), Course("PHP"), Course("C++") ) var resultCourses = OddList(courses).oddItems() println(resultCourses) } |
3. Covariance
- If C <T> is a generic type whose parameter T and U are subtypes of T, then C <U> is a subtype of C <T>
- For example, List <Int> is a subtype of List <Number> because Int is a subtype of Number.
- Applies to types that are “producers”, or “sources” of T
- T only appears in the “out” position, that is, the return type of the function
Code Sample:
1 2 3 4 5 6 7 8 | class CovarianceSample<T> fun main() { val firstSample: CovarianceSample<Any> = CovarianceSample<Int>() // Error: Type mismatch val secondSample: CovarianceSample<out Any> = CovarianceSample<String>() // OK , String is a subtype of Any val thirdSample: CovarianceSample<out String> = CovarianceSample<Any>() // Error: Type mismatch } |
4. Contravariance
- If C <T> is a generic type whose parameter T and U are subtypes of T, then C <T> is a subtype of C <U>
- U is the subtype of T ⇒ C <T> is the subtype of C <U>
- For example: Function1 <Number, Int> is a subtype of Function1 <Int, Int> because Int is a subtype of Number.
- Applies to styles that are “consumers” of T.
- T only appears in the “in” position, that is, the type of the function argument
Code Sample:
1 2 3 4 5 6 7 8 9 | open class Vehicle class Bicycle : Vehicle() class Container<in T> fun main() { var containerBicycle: Container<Bicycle> = Container<Vehicle>() // OK var containerVehicle: Container<Vehicle> = Container<Bicycle>() // Error: Type mismatch } |
5. Invariance
- If C <T> is a subtype of C <U>, then T = U
- For example: Array <T> is invariant in T
- T appears in both “in” and “out” positions.
- Type is both a producer and a consumer of T
- Remember: Lambdas are contra-variant in argument types and covariant in their return types.
6. Type projections
- The projection type is a type that has been limited in certain ways to obtain variance characteristics by using the use-site variance.
7. Star projection
- They are useful when we know nothing about the type of arguments, but need to use them safely.
- The safe way here is to define a generic type projection, that every specific initialization of that generic type will be a subtype type of that projection.
- Instead of using C <in Nothing> or C <out Any?>, You can use C <*>. It creates an effective interface similar to the other two approaches:
- So we have three possible ways of accepting any kind of generic:
- in-projection – <in Nothing>
- out-projection – <out Any?>
- star-projection – <*>
Code Sample:
1 2 3 | val languages = arrayOf("Kotlin", "Java", "Generics", 1) printArray(languages) |
8. Type erasure and reified type parameters
- Java has restrictions on types that are considered to be re-determinable – meaning they are completely available at run time (see Java SE for reifiable types).
- The type safety tests that Kotlin performs for the use of generic declarations are only performed at compile time. At runtime, generic type instances do not contain any information about their actual type arguments.
- Execute type constraints only at compile time and remove element type information at runtime.
- The type information is assumed to be deleted.
- In the case of type parameters in Kotlin, we can compare types and get Class objects.
- Reified types parameters:
- Only works with functions (or extension properties with function get ())
- Working with functions is declared as inline
- Advantages of using Reified type parameters:
- Type checking with is
- casts without cast unchecked warning
- assign class objects by appending :: class.java to the parameter name. For example: val a = T :: class.java
Code Sample:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | // single param inline fun <reified T> Any.isInstanceOf(): Boolean = this is T fun main() { val isStringAString = "String".isInstanceOf<String>() val isIntAString = 1.isInstanceOf<String>() } // multiple params inline fun <reified T, reified U> haveSameType(first: T, second: U) = first is U && second is T // extension properties, but the type parameter is used as the receiver type inline val <reified T> T.theClass get() = T::class.java |