Unchecked Cast в Kotlin: Как безопасно привести List<Any?> к List<Waypoint>

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

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

Начать курс

«Несовместимые типы»: почему 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 всё упадёт.

Частые вопросы

Блоги

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