Generics in Kotlin

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

👩🏻‍💻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)
val listOfInts = listOf(1, 7, 8, 9, 12, 45)
val resultOfInts = OddList(listOfInts)
val courses = listOf(
var resultCourses = OddList(courses).oddItems()


  • 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

👩🏻‍💻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


  • 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

👩🏻‍💻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


  • 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

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.

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<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 – <*>

👩🏻‍💻Code sample

val languages = arrayOf("Kotlin", "Java", "Generics", 1)

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 to the parameter name. For example: val a =

👩🏻‍💻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() =

📚 Learn more

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! 🙌🙏😍✌

Follow me on: Twitter  | Medium |

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s