«Несовместимые типы»: почему Kotlin кричит на ваш List?
Вы когда-нибудь сталкивались с ситуацией, когда Kotlin, этот «умный» и «безопасный» язык, вдруг выдаёт вам жёлтое предупреждение Unchecked Cast: kotlin.collections.List<Kotlin.Any?> to kotlin.collections.List<Waypoint>? Если да — вы не одиноки. Этот вопрос на Stack Overflow собрал тысячи просмотров, и это неудивительно. Каждый, кто переходит с Java или начинает глубже работать с дженериками в Kotlin, рано или поздно натыкается на эту «граблю».
В этой статье мы не просто дадим сухое объяснение. Мы разберём, почему так происходит, покажем три способа правильно решить проблему и, самое главное, — как писать код, который не взорвётся в рантайме. Готовы? Поехали!
Суть проблемы: Type Erasure и «стирание» типов
Давайте сразу к делу. Представьте, что у вас есть функция, которая возвращает список чего угодно — List<Any?>. А вам нужен список конкретных объектов, например, List<Waypoint>. Самое «простое» решение, которое приходит в голову — это явное приведение:
val rawList: List<Any?> = getDataFromSomewhere() // Допустим, это JSON или база данных
val waypointList: List<Waypoint> = rawList as List<Waypoint> // Unchecked Cast! ⚠️
Kotlin (и Java под капотом) использует механизм Type Erasure (стирание типов). Это значит, что во время выполнения программы информация о типе внутри дженериков (то, что в угловых скобках) стирается. Для JVM ваш List<Waypoint> — это просто List. Компилятор видит, что вы пытаетесь обмануть систему, и выдаёт предупреждение: «Эй, я не могу проверить, что там действительно Waypoint, ты берёшь на себя ответственность!».
Если вы уверены на 100%, что внутри только Waypoint, вы можете подавить предупреждение аннотацией @Suppress("UNCHECKED_CAST"). Но если там окажется, скажем, String — вы получите ClassCastException в рантайме. А это именно то, от чего нас пытается защитить Kotlin.
Как НЕ надо делать: типичные ошибки новичков
Вот несколько примеров, которые я часто вижу на форумах. Запомните их, чтобы не повторять:
- Слепое приведение:
list as List<Waypoint>— самый опасный вариант. - Игнорирование предупреждения: «Работает же!» — работает, пока не упадёт.
- Попытка привести в цепочке:
(list as? List<Waypoint>)— безопасный операторas?не поможет, если внутри списка лежат не те объекты. Он проверит только то, что этоList, но не его содержимое.
3 рабочих способа: от простого к профессиональному
Давайте посмотрим на реальные примеры кода. Предположим, у нас есть класс Waypoint:
data class Waypoint(val latitude: Double, val longitude: Double, val name: String)
И есть «грязный» список, полученный из внешнего API:
fun fetchRawData(): List<Any?> {
return listOf(
mapOf("lat" to 55.75, "lon" to 37.62, "name" to "Moscow"),
mapOf("lat" to 59.93, "lon" to 30.31, "name" to "SPb")
)
}
Способ 1: Фильтрация с filterIsInstance (Рекомендуемый ✅)
Это самый идиоматичный и безопасный способ. Функция filterIsInstance<T>() отфильтрует только те элементы, которые являются экземплярами указанного типа, и вернёт уже типизированный список.
fun getWaypointsSafely(): List<Waypoint> {
val rawData: List<Any?> = fetchRawData()
// Допустим, мы каким-то образом преобразуем Map в Waypoint
val waypoints: List<Waypoint> = rawData
.filterIsInstance<Map<*, *>>()
.map { map ->
Waypoint(
latitude = map["lat"] as? Double ?: 0.0,
longitude = map["lon"] as? Double ?: 0.0,
name = map["name"] as? String ?: "Unknown"
)
}
return waypoints
}
Почему это круто? Если в списке вдруг окажется String или Int — они просто будут проигнорированы. Никаких исключений. filterIsInstance делает всю грязную работу за вас.
Способ 2: Явное приведение с проверкой (Безопасное, но многословное)
Если вы точно знаете структуру данных, можно использовать map с явным приведением каждого элемента. Это даёт полный контроль над ошибками.
fun getWaypointsWithCheck(): List<Waypoint> {
val rawData: List<Any?> = fetchRawData()
return rawData.mapNotNull { item ->
when (item) {
is Map<*, *> -> {
try {
Waypoint(
latitude = (item["lat"] as? Double) ?: return@mapNotNull null,
longitude = (item["lon"] as? Double) ?: return@mapNotNull null,
name = (item["name"] as? String) ?: return@mapNotNull null
)
} catch (e: Exception) {
println("Ошибка парсинга: ${e.message}")
null // Пропускаем битые данные
}
}
else -> null // Пропускаем всё, что не Map
}
}
}
Этот код длиннее, но он «пуленепробиваемый». Каждый элемент проверяется, и если что-то пошло не так — элемент просто исключается из списка.
Способ 3: Подавление предупреждения (Только для крайних случаев 🚨)
Иногда вы работаете с легаси-кодом или библиотекой, где типы гарантированы на 100%. В этом случае можно подавить предупреждение, но делайте это осознанно.
@Suppress("UNCHECKED_CAST")
fun getWaypointsUnsafe(): List<Waypoint> {
val rawData: List<Any?> = fetchRawData()
// Мы уверены, что rawData всегда состоит из Waypoint
return rawData as List<Waypoint>
}
ВАЖНО: Используйте этот подход только если:
- Вы контролируете источник данных.
- Вы провели валидацию выше по коду.
- Вы готовы к тому, что при малейшем изменении API всё упадёт.