Introduction: Why Generics Are Not Scary, But Convenient
\n\nImagine 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?
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\nThis article is prepared for the educational portal pythonlib.ru — your guide to the world of modern programming languages.
\n\n1. Basics: What Are Generics in Kotlin?
\n\nGenerics (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\nTake a look at a simple example:
\n\nclass 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\nKey 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
Generic Functions
\nTyping in Kotlin also works for functions. For example, a function that returns the first element of a list:
\n\nfun <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\nNote: Kotlin can infer the type from context — writing firstItem<Int>(numbers) is not necessary.
2. Variance: in and out — How Not to Break Type Safety
\n\nThe most complex topic for beginners is variance. It answers the question: can List<String> be a subtype of List<Any>?
In Java, this is solved with wildcards (? extends / ? super). In Kotlin — with out and in.
out (covariance) — read-only
\nThe out annotation means the generic type can only be "produced" (returned from methods). Example — List<out T>.
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\nWhy is this safe? If we can only read String from the source, it can be considered a source of Any.
in (contravariance) — write-only
\nin is the opposite: the type can only be on the input. A classic example is Comparable.
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\nIf Comparable<Number> can compare any numbers, it will certainly compare Int as well.
in out (invariance) — default
\nIf neither in nor out is specified, the type is inv