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.
2️⃣Variance
Variance refers to how subtyping between more complex types relates to subtyping between their components.
Conventions: E = element | T = type | K = key | V = value
👩🏻💻Code sample
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 is a generic type with type parameter T and U is a subtype of T, then C is a subtype of C
- Example: List is a subtype of List 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
👩🏻💻Code sample
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 is a generic type with type parameter T and U is a subtype of T, then C is a subtype of C
- U subtype of T ⇒ C subtype of C
- Example: Function1 is a subtype of Function1 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
👩🏻💻Code sample
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 is a subtype of C, then T = U
- Example: Array 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
6️⃣Type projections
- A type projection is a type that has been limited in certain ways in order to gain variance characteristics using use-site variance.
👩🏻💻Code sample
class KotlinConsumer<in T> { | |
fun toString(value: T): String { | |
return value.toString() | |
} | |
} | |
fun main() { | |
val inValue: KotlinConsumer<Number> = KotlinConsumer() | |
val copyOfInValue: KotlinConsumer<Int> = inValue | |
if (copyOfInValue is KotlinConsumer<Int>) { | |
println(copyOfInValue.toString()) | |
} | |
} |
class KotlinProducer<out T>(val value: T) { | |
init { | |
println(value) | |
} | |
} | |
fun main() { | |
val outValue = KotlinProducer("Just a string") | |
val copyOfOutValue: KotlinProducer<Any> = outValue | |
println(copyOfOutValue.value.toString()) | |
} |
7️⃣Star projection
- 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 or C, 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 —
- out-projection —
- star-projection — <*>
👩🏻💻Code sample
val languages = arrayOf("Kotlin", "Java", "Generics", 1) | |
printArray(languages) |
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 of 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
👩🏻💻Code sample
// 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 |
📚 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 it!
Thank you for reading! 🙌🙏😍✌
Follow me on:
Originally published at http://magdamiu.com on June 21, 2020.
Top comments (0)