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

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

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

Начать курс

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

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

Проблема, которую решают Generics

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

fun getFirstString(list: List<String>): String? = list.firstOrNull()
fun getFirstInt(list: List<Int>): Int? = list.firstOrNull()
fun getFirstUser(list: List<User>): User? = list.firstOrNull()

Это приводит к дублированию кода. Использование Any решит проблему, но вы потеряете информацию о типе на этапе компиляции:

fun getFirst(list: List<Any>): Any? = list.firstOrNull()

// Придется делать приведение типов
val name: String? = getFirst(strings) as? String

Generics решают эту проблему элегантно:

fun <T> getFirst(list: List<T>): T? = list.firstOrNull()

// Компилятор знает тип!
val name: String? = getFirst(listOf("Alice", "Bob"))
val age: Int? = getFirst(listOf(25, 30))

Теперь одна функция работает с любым типом, сохраняя при этом полную типобезопасность.

Основы Generics в Kotlin: синтаксис и объявление

Обобщения объявляются с помощью угловых скобок <T>. Буква T — это просто соглашение, можно использовать любые имена: E (элемент), K (ключ), V (значение).

Generic-классы

class Box<T>(val value: T) {
    fun getContent(): T = value
}

fun main() {
    val stringBox = Box("Hello")
    val intBox = Box(42)
    
    println(stringBox.getContent()) // Hello
    println(intBox.getContent())    // 42
}

Generic-функции

fun <T> printItem(item: T) {
    println("Item: $item")
}

// Можно указывать тип явно
printItem<String>("Kotlin")
// Или полагаться на вывод типов
printItem(100)

Generic-интерфейсы

interface Repository<T> {
    fun getById(id: Int): T
    fun save(item: T): Boolean
}

class UserRepository : Repository<User> {
    override fun getById(id: Int): User { /* ... */ }
    override fun save(item: User): Boolean { /* ... */ }
}

Вариативность: in, out и звездочка *

Одна из самых мощных возможностей Kotlin — вариативность (variance). Она отвечает на вопрос: может ли List<String> быть подтипом List<Any>?

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

Используйте out, когда ваш generic-тип только возвращается (producer). Например, List<T> — ковариантен:

// Вы можете присвоить List<String> переменной List<Any>
val strings: List<String> = listOf("a", "b")
val anyList: List<Any> = strings // OK!

// Но не можете добавлять элементы
// anyList.add(1) // Ошибка компиляции!

Собственный пример:

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

fun main() {
    val stringProducer: Producer<String> = Producer("Hello")
    val anyProducer: Producer<Any> = stringProducer // Работает!
    println(anyProducer.produce())
}

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

Используйте in, когда тип только принимается (consumer). Например, Comparable<T>:

// Comparable<Any> может быть присвоен Comparable<String>
val anyComparable: Comparable<Any> = object : Comparable<Any> {
    override fun compareTo(other: Any): Int = 0
}
val stringComparable: Comparable<String> = anyComparable // OK!

Собственный пример:

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

fun main() {
    val anyConsumer: Consumer<Any> = Consumer()
    val stringConsumer: Consumer<String> = anyConsumer // Работает!
    stringConsumer.consume("Kotlin")
}

Star-projection (*)

Если точный тип неизвестен, используйте *:

fun printLength(list: List<*>) {
    println(list.size)
}

// Можно передать любой список
printLength(listOf(1, 2, 3))
printLength(listOf("a", "b", "c"))

Практические примеры использования Generics

1. Типобезопасный билдер

class Response<T>(val data: T, val message: String)

fun <T> buildResponse(data: T, message: String = "OK"): Response<T> {
    return Response(data, message)
}

fun main() {
    val userResponse = buildResponse(User("Alice"))
    val errorResponse = buildResponse(null, "Error")
    
    // Компилятор знает, что userResponse.data — это User
    println(userResponse.data.name)
}

2. Ограничение типов (type bounds)

// T должен быть подтипом Number
fun <T : Number> sum(list: List<T>): Double {
    return list.sumOf { it.toDouble() }
}

fun main() {
    println(sum(listOf(1, 2, 3)))       // 6.0
    println(sum(listOf(1.5, 2.5)))       // 4.0
    // sum(listOf("a", "b")) // Ошибка! String не является Number
}

3. Reified generics (для inline-функций)

В Kotlin можно получить доступ к типу во время

Блоги

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