Generics in Kotlin: How to Stop Fearing Angle Brackets and Write Clean Code

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Introduction: Why Generics Are Not Scary, But Convenient

\n\n

Imagine you are writing a "Box" class. You can put anything in it: a book, an apple, a phone. But if you make it for Object, every time you take something out you'll have to do as? Book and pray you don't get a ClassCastException. Sound familiar?

\n\n

This is where generics come to the rescue — a powerful typing mechanism in Kotlin that allows you to write flexible code without sacrificing type safety. In this article, we'll break down generics in Kotlin from simple to complex: basic classes, functions, variance (in/out), reified types, and star projections. You'll learn how to make your code cleaner and the compiler your main ally.

\n\n

This article is prepared for the educational portal pythonlib.ru — your guide to the world of modern programming languages.

\n\n

1. Basics: What Are Generics in Kotlin?

\n\n

Generics (generic types) allow you to parameterize classes, interfaces, and functions with a type that will be specified at the time of use. This eliminates code duplication and gives you control over types at compile time.

\n\n

Take a look at a simple example:

\n\n
class Box<T>(val item: T)\n\nfun main() {\n    val bookBox = Box<String>("Kotlin in Action")\n    val intBox = Box<Int>(42)\n    // The compiler knows the type!\n    val title: String = bookBox.item\n}
\n\n

Key points:

\n
    \n
  • <T> — type parameter (can be any letter, but by convention T, R, E, K, V).
  • \n
  • The type is fixed when the object is created — no casts at runtime.
  • \n
  • Generics work only at compile time (type erasure), but Kotlin adds "reification" (more on this later).
  • \n
\n\n

Generic Functions

\n

Typing in Kotlin also works for functions. For example, a function that returns the first element of a list:

\n\n
fun <T> firstItem(list: List<T>): T? = list.firstOrNull()\n\nfun main() {\n    val numbers = listOf(1, 2, 3)\n    val first: Int? = firstItem(numbers) // T = Int\n    println(first) // 1\n}
\n\n

Note: Kotlin can infer the type from context — writing firstItem<Int>(numbers) is not necessary.

\n\n

2. Variance: in and out — How Not to Break Type Safety

\n\n

The most complex topic for beginners is variance. It answers the question: can List<String> be a subtype of List<Any>?

\n\n

In Java, this is solved with wildcards (? extends / ? super). In Kotlin — with out and in.

\n\n

out (covariance) — read-only

\n

The out annotation means the generic type can only be "produced" (returned from methods). Example — List<out T>.

\n\n
interface Source<out T> {\n    fun next(): T  // OK: T on output\n    // fun add(item: T) // Error! T on input is forbidden\n}\n\nfun demo(source: Source<String>) {\n    val anySource: Source<Any> = source // out allows this\n}
\n\n

Why is this safe? If we can only read String from the source, it can be considered a source of Any.

\n\n

in (contravariance) — write-only

\n

in is the opposite: the type can only be on the input. A classic example is Comparable.

\n\n
interface Comparable<in T> {\n    operator fun compareTo(other: T): Int\n}\n\nfun demo(comp: Comparable<Number>) {\n    val intComp: Comparable<Int> = comp // in allows this\n}
\n\n

If Comparable<Number> can compare any numbers, it will certainly compare Int as well.

\n\n

in out (invariance) — default

\n

If neither in nor out is specified, the type is inv

Blogs

Book Recommendations