Generics в Kotlin: зачем нужны и как использовать

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение в Generics в Kotlin

Generics (обобщения) — одна из ключевых фишек языка Kotlin, которая позволяет писать гибкий, типобезопасный и переиспользуемый код. Если вы когда-нибудь писали List<String> или Map<Int, User>, вы уже использовали дженерики, даже не задумываясь. Но что стоит за этими угловыми скобками и как выжать из них максимум? В этой статье мы разберем, зачем нужны Generics в Kotlin, как их применять на практике и какие продвинутые фичи (variance, reified, where) помогут вам писать еще более чистый и безопасный код.

Эта статья предназначена для разработчиков, которые уже знакомы с основами Kotlin и хотят углубить свои знания. Мы рассмотрим всё: от базового синтаксиса до реальных примеров из production-кода.

Зачем нужны Generics? Проблема без них

Представьте, что вам нужно написать функцию, которая принимает список любых элементов и возвращает первый элемент. Без дженериков вы бы написали так:

fun getFirst(list: List<Any>): Any {
    return list[0]
}

Теперь, если вы передадите List<String>, то получите Any на выходе. Чтобы работать с ним как со строкой, придется делать явное приведение типов:

val first = getFirst(listOf("Kotlin", "Java")) as String
first.length // Опасно! Может упасть ClassCastException

Это неудобно и небезопасно. Ошибка проявится только в рантайме. Generics решают эту проблему, сохраняя информацию о типе на этапе компиляции.

Основные преимущества Generics:

  • Типобезопасность — компилятор проверяет типы на этапе компиляции, а не в рантайме.
  • Устранение явных приведений — код становится чище и читаемее.
  • Переиспользуемость — один класс или функция работают с разными типами.

Базовый синтаксис: классы, функции и ограничения

Обобщенные классы

Самый простой способ начать — объявить класс с одним или несколькими параметрами типа. Параметры типа указываются в угловых скобках <T> (буква может быть любой, но традиционно используют T, E, K, V):

class Box<T>(val value: T)

fun main() {
    val stringBox = Box("Kotlin")   // Тип выводится автоматически
    val intBox = Box(42)            // Box<Int>

    println(stringBox.value.length) // Работает без приведения!
    println(intBox.value + 1)       // 43
}

Kotlin умеет выводить тип, поэтому Box<String> писать не обязательно — достаточно Box("Kotlin").

Обобщенные функции

Функции тоже могут быть обобщенными. Параметр типа указывается перед именем функции:

fun <T> getMiddleElement(list: List<T>): T? {
    if (list.isEmpty()) return null
    return list[list.size / 2]
}

fun main() {
    val numbers = listOf(1, 2, 3, 4, 5)
    val middle = getMiddleElement(numbers) // Тип Int? выводится автоматически
    println(middle) // 3
}

Ограничения (where / upper bound)

Иногда нужно ограничить, какие типы можно использовать в дженерике. Например, чтобы вызывать метод .compareTo(), тип должен реализовывать интерфейс Comparable:

fun <T : Comparable<T>> maxOfTwo(a: T, b: T): T {
    return if (a >= b) a else b
}

fun main() {
    println(maxOfTwo(10, 20))   // 20
    println(maxOfTwo("A", "B")) // B
    // maxOfTwo(10, "A")        // Ошибка компиляции: типы не совпадают
}

Если нужно несколько ограничений, используйте ключевое слово where:

interface Printable {
    fun print()
}

fun <T> process(item: T) where T : Comparable<T>, T : Printable {
    if (item > something) item.print()
}

Вариативность (Variance): in, out и проекции

Одна из самых сложных тем в дженериках — вариативность. Она отвечает на вопрос: если Box<Cat> — это подтип Box<Animal>? В Kotlin ответ зависит от того, как объявлен параметр типа.

Ковариантность (out)

Если вы только читаете данные из класса (производите их), используйте out. Это делает класс ковариантным: Box<Cat> становится подтипом Box<Animal>.

class Producer<out T>(private val value: T) {
    fun get(): T = value
}

fun main() {
    val catProducer: Producer<Cat> = Producer(Cat())
    val animalProducer: Producer<Animal> = catProducer // Корректно!
    val animal: Animal = animalProducer.get()
}

Жизненный пример: List<T> в Kotlin — ковариантен (объявлен как List<out T>). Поэтому вы можете присвоить List<String> переменной типа List<Any>.

Контравариантность (in)

Если вы только записываете данные в класс (потребляете их), используйте in. Это делает класс контравариантным: Box<Animal> становится подтипом Box<Cat>.

class Consumer<in T> {
    fun consume(value: T) {
        println("Consumed: $value")
    }
}

fun main() {
    val animalConsumer: Consumer<Animal> = Consumer()
    val catConsumer: Consumer<Cat> = animalConsumer // Корректно!
    catConsumer.consume(Cat())
}

Жизненный пример: Comparable<T> — контравариантен (объявлен как Comparable<in T>). Если

Блоги

Рекомендаций по книгам