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 можно получить доступ к типу во время