Chapter 20: Generics

Generics — this is one of the most powerful, elegant, and brain-expanding features in Kotlin (and modern programming in general).

Generics let you write type-safe, reusable code that works with any type — while still getting full compile-time safety and no runtime overhead. Once you master generics, you’ll feel like you’ve unlocked a superpower — especially when working with collections, repositories, mappers, or any reusable logic.

We’re going to go super slowly, like we’re sitting together in a cozy Mumbai café — I’ll explain every concept with real-life analogies, complete runnable examples, step-by-step breakdowns, tables, common mistakes with fixes, and tons of practical code you can copy-paste and run right now.

Let’s dive in! ☕🚀

1. Generic Classes & Functions – The Basics

Generic classes and generic functions use type parameters (usually T, E, K, V, R, etc.) to represent any type.

Example 1: Generic Class – A Simple Box

Kotlin

Step-by-step:

  1. class Box<T> → T is a placeholder for any type
  2. When you create Box<String>, T becomes String
  3. The compiler ensures type safety — you can’t put Int in a Box<String>

Example 2: Generic Function

Kotlin

Generic function syntax:

  • <T> before the function name (type parameter declaration)
  • Can be used in parameters, return type, etc.

2. Variance – out (Covariant) vs in (Contravariant)

Variance answers the question: “If I have a Box<Cat>, can I use it as a Box<Animal>?”

Kotlin uses declaration-site variance (you decide once when writing the class/function).

Variance Keyword Meaning Safe for Example Use Case Real-life Analogy
Covariant (out) out T Producer — you only read T Reading (get) List<T>, Sequence<T> A fruit basket: if it contains apples, you can treat it as a fruit basket (Apple is a Fruit)
Contravariant (in) in T Consumer — you only write T Writing (set) Comparator<T>, Consumer<T> A trash bin: if it accepts fruits, it can accept apples (more general type is safe)
Invariant (default) T Neither read nor write safely Both MutableList<T>, Array<T> A locked box: only works with exact type

Example 1: Covariant (out) – Reading is safe

Kotlin

Why safe? You can only get items → even if you treat it as Producer<Animal>, you’ll only get Animals (Cats are Animals).

Example 2: Contravariant (in) – Writing is safe

Kotlin

Why safe? You can only put items in → even if you treat it as Consumer<Cat>, you can safely put a Cat (because it accepts Animals, and Cat is Animal).

Example 3: Invariant (default) – Neither safe

Kotlin

Rule of thumb (PECS – Producer Extends, Consumer Super):

  • If you produce (get/output) T → use out T (covariant)
  • If you consume (set/input) T → use in T (contravariant)
  • If you do both → leave invariant (T)

3. Star Projection (*) – When You Don’t Care About the Type

Star projection (*) means “I don’t know/care what the type parameter is”.

Use cases:

  • When you only read and don’t care about the type
  • When you want to accept any generic type

Example:

Kotlin

Star projection rules:

  • Collection<*> → you can read as Any?
  • MutableList<*> → you cannot read or write (only size, isEmpty, etc.)

4. Reified Type Parameters – Knowing the Type at Runtime

Normally, generics are erased at runtime (type parameters disappear — T becomes Any?).

reified lets you keep the type information at runtime — only possible in inline functions.

Syntax:

Kotlin

Example:

Kotlin

Very common use case: Generic repository / parser functions that need to know the exact type.

Limitation: reified only works in inline functions — the compiler inlines the code and keeps the type info.

Quick Recap Table (Your Cheat Sheet)

Feature Syntax / Example Key Benefit
Generic class class Box<T>(val item: T) Type-safe reusable container
Generic function fun <T> print(item: T) Flexible for any type
Covariant (out) class Producer<out T> Safe for reading (get)
Contravariant (in) class Consumer<in T> Safe for writing (set)
Star projection Collection<*> Accept any type (read as Any?)
Reified inline fun <reified T> … Know type at runtime (no erasure)

Common Newbie Mistakes & Fixes

Mistake Problem Fix
Using out when you need to write Compile error when setting Use invariant (T) or in
Using in when you need to read Can’t safely read (gets Any?) Use out or invariant
Forgetting reified in inline function needing type info Type erasure – is T fails Add reified to type parameter
Using * and trying to add elements Compile error – unknown type Use in T if you need to write
Not using out on read-only collections Less flexible Mark List<out T> (already done in stdlib)

Homework for You (Let’s Make It Fun!)

  1. Basic Create generic class Pair<T, U>(val first: T, val second: U) with swap() function that returns reversed pair.
  2. Medium Create covariant generic class Producer<out T>(val item: T) → show you can assign Producer<Cat> to Producer<Animal>.
  3. Advanced Create contravariant generic class Consumer<in T> with consume(value: T) → show Consumer<Animal> can be used as Consumer<Cat>.
  4. Fun Create reified inline function inline fun <reified T> List<*>.countOfType() → returns how many elements are of type T.
  5. Challenge Fix this code:
    Kotlin

You’ve just mastered Kotlin’s generics system — now you can write type-safe, flexible, and high-performance code!

You may also like...

Leave a Reply

Your email address will not be published. Required fields are marked *