JavaRush /Курсы /Kotlin SELF /mapNotNull и filterNotNull: очистка данных и смена типов

mapNotNull и filterNotNull: очистка данных и смена типов

Kotlin SELF
22 уровень , 3 лекция
Открыта

1. Почему нам вообще мешает null в коллекциях

Когда вы только начинаете программировать, null кажется полезной штукой: «ну нет значения — значит null». Проблема в том, что null — это не просто «пусто», а отдельное значение, которое нужно постоянно учитывать. В Kotlin это усилено системой типов: String и String? — это разные типы, и компилятор не даёт вам притворяться, что null «сам как-нибудь рассосётся».

В коллекциях null становится особенно неприятным гостем по двум причинам. Во‑первых, он портит тип результата и заставляет нас писать проверки в местах, где по смыслу «всё уже должно быть нормальным». Во‑вторых, null часто появляется как результат безопасного парсинга (toIntOrNull()), поиска (find) или безопасного доступа (getOrNull). То есть он приходит не потому, что мы «любим null», а потому что мы пишем устойчивый код.

Давайте посмотрим на мини-пример, который очень похож на реальность: мы получили список строк, где кто-то написал числа, а кто-то — «ой». Мы хотим получить список чисел.

fun main() {
    val raw = listOf("10", "x", "7")
    val parsed = raw.map { it.toIntOrNull() }

    println(parsed) // [10, null, 7]
}

Результат формально честный: List<Int?>. Но дальше вы почти наверняка захотите посчитать сумму или сравнить элементы — и внезапно окажется, что это не Int, а Int?, и просто так «прибавить» нельзя. Именно здесь и появляется «уборка null» как отдельный шаг обработки данных.

2. mapNotNull: превращаем и одновременно выкидываем null

Если map — это «преобразуй каждый элемент», то mapNotNull — это «преобразуй каждый элемент, но если получилось null, просто выброси этот результат». Это звучит как маленькое улучшение, но на практике оно экономит много нервов, потому что вы получаете non-null список и можете дальше работать без постоянных проверок.

В документации стандартной библиотеки эта идея формулируется прямо: если преобразование может давать null, вместо map() используйте mapNotNull(). Это ровно наш случай с toIntOrNull().

Начнём с очень маленького примера: будем умножать числа, но 2 считаем подозрительной и превращаем в null (просто чтобы увидеть механику).

fun main() {
    val numbers = setOf(1, 2, 3)

    val result = numbers.mapNotNull { x ->
        if (x == 2) null else x * 3
    }

    println(result) // [3, 9]
}

Обратите внимание на две вещи. Во-первых, mapNotNull возвращает список, в котором нет null. Во-вторых, тип результата становится не List<Int?>, а List<Int> — и это главный выигрыш.

Мини‑схема в голове

mapNotNull можно воспринимать как «map + фильтрация по not-null», только записанную короче и яснее по смыслу:

элемент T --(transform)--> R?  --(если null, выкинуть)--> R

И это не философия, а реальный практический контракт функции: «преобразовать и отбросить null-результаты».

3. Парсинг и очистка «грязного» ввода

Сейчас будет ситуация, которая случается в консольных программах чаще, чем «здравый смысл»: пользователь ввёл что-то не то. И наша задача — не упасть, не устроить драму, а спокойно вытащить из этого хотя бы полезные кусочки.

toIntOrNull() — базовый инструмент безопасного парсинга: если строка не число, он возвращает null, а не кидает исключение. Этот паттерн («прочитал → попробовал распарсить → получил T?») вы уже видели, и он считается идиоматичным для Kotlin.

Пример: нормализация + парсинг

Допустим, у нас есть «сырые» строки: пробелы, пустые строки, мусор. Мы хотим получить список чисел.

fun main() {
    val raw = listOf("10", " x ", " 7 ", "")
    val numbers = raw
        .map { it.trim() }
        .mapNotNull { it.toIntOrNull() }

    println(numbers) // [10, 7]
}

Здесь важно, что trim() — это просто «косметика», а ключевой шаг — именно mapNotNull { it.toIntOrNull() }. Он превращает список строк в список чисел и выкидывает всё, что не распарсилось.

Если вы вдруг подумали: «а где сообщение пользователю, что он ввёл мусор?» — отличный вопрос. Но мы в этой лекции держим фокус на типах и чистке данных. Сообщения можно делать отдельным шагом (и чаще всего это действительно отдельная логика, иначе вы получите печать внутри map, а это мы стараемся не любить).

Почему не map { ... }.filterNotNull()?

Можно и так. Но mapNotNull лучше по двум причинам: короче и точнее описывает намерение («я понимаю, что из преобразования может выйти null, и я хочу его выкинуть»). В документации это и подаётся как прямой инструмент для такого случая.

Покажу оба варианта рядом на одном и том же примере (не как «правильно/неправильно», а как «более прямое выражение мысли»).

fun main() {
    val raw = listOf("1", "oops", "3")

    val a = raw.map { it.toIntOrNull() }.filterNotNull()
    val b = raw.mapNotNull { it.toIntOrNull() }

    println(a) // [1, 3]
    println(b) // [1, 3]
}

Оба дадут один результат. Но mapNotNull читается как единая операция: «парсим числа, мусор игнорируем».

Небольшая блок-схема пайплайна

Иногда полезно буквально нарисовать, что происходит:

flowchart TD
    A[Сырые строки] --> B["trim()"]
    B --> C["toIntOrNull()"]
    C -->|Int| D[в список результатов]
    C -->|null| E[выкинуть]
    D --> F[List<Int>]

Эта схема — «внутренний мир» mapNotNull в конкретном кейсе.

filterNotNull(): убираем null из уже nullable-коллекции

Если mapNotNull спасает нас, когда null появляется во время преобразования, то filterNotNull() полезен, когда null уже сидит в коллекции, и нам нужно просто его вычистить. Это другой сценарий: мы ничего не преобразуем, мы просто говорим: «оставь только не-null».

В документации по фильтрации коллекций отдельно подчёркивается, что filterNotNull() возвращает все non-null элементы и, что особенно приятно, сужает тип: из List<T?> получается List<T>.

Пример: список имён, где иногда null.

fun main() {
    val maybeNames: List<String?> = listOf("Ann", null, "Bob")
    val names: List<String> = maybeNames.filterNotNull()

    println(names) // [Ann, Bob]
}

Здесь главный эффект даже не в том, что null исчез, а в том, что исчез ? из типа. Теперь вы можете спокойно делать names.map { it.length }, и компилятор не будет напоминать вам о бренности бытия.

Важная граница: filterNotNull() не удаляет «пустое», он удаляет именно null.

Это частая путаница. Пустая строка "" — это не null. Если у вас список listOf("", "Bob"), то filterNotNull() ничего не изменит. Он не про «плохие данные», он именно про значение null.

4. Очистка данных в мини‑бюджет‑трекере

Сейчас мы сделаем несколько небольших шагов, которые выглядят как «мелочь», но на практике превращают консольное приложение из «сломалось на первом же вводе» в «живёт в реальном мире».

Представим, что у нас уже есть простейший трекер расходов, где расход — это Triple(category, note, amount). Да, это не идеально, но мы пока сознательно без ООП-моделей: нам важнее научиться обрабатывать коллекции.

Парсим «импорт сумм» одной строкой с mapNotNull

Допустим, мы хотим команду вроде: пользователь вводит строку с числами через пробел, а мы добавляем их как расходы в категорию "import". Ошибочные куски игнорируем.

fun parseInts(line: String): List<Int> =
    line.split(" ").mapNotNull { it.trim().toIntOrNull() }

fun main() {
    val nums = parseInts("10  x  7   -3")
    println(nums) // [10, 7, -3]
}

Обратите внимание, что мы не делаем «умные» правила (например, запрещаем отрицательные). Сегодня мы тренируем механику mapNotNull. Правила валидации можно добавить прямо сюда, возвращая null, когда число «не подходит».

Вот как это выглядит, если мы хотим пропускать всё, что меньше или равно нулю:

fun parsePositiveInts(line: String): List<Int> =
    line.split(" ").mapNotNull { token ->
        val n = token.trim().toIntOrNull()
        if (n != null && n > 0) n else null
    }

fun main() {
    val nums = parsePositiveInts("10  x  7   -3  0")
    println(nums) // [10, 7]
}

Ключевая мысль: раз mapNotNull выкидывает null, вы можете использовать null как «сигнал отбраковки». Это намного удобнее, чем копить список вручную и писать кучу if.

Извлекаем суммы расходов, где часть записей может быть «битой», через filterNotNull

Теперь представим другой сценарий: у нас уже есть список расходов, но где-то по дороге мы получили «битые» элементы. Например, у нас есть функция, которая пытается распарсить строку команды в расход и возвращает Triple<String, String, Int>?: если не смогла — null.

Это очень реалистично: парсер почти всегда возвращает nullable.

fun parseExpenseOrNull(line: String): Triple<String, String, Int>? {
    val parts = line.trim().split(" ")
    val amount = parts.getOrNull(2)?.toIntOrNull()
    if (parts.size < 3 || amount == null) return null
    return Triple(parts[0], parts[1], amount)
}

fun main() {
    val rawLines = listOf(
        "food pizza 500",
        "oops",
        "taxi ride 250"
    )

    val parsed: List<Triple<String, String, Int>?> = rawLines.map(::parseExpenseOrNull)
    val expenses: List<Triple<String, String, Int>> = parsed.filterNotNull()

    println(expenses.size) // 2
}

Здесь filterNotNull() превращает «список возможно-расходов» в «список точно-расходов». И это тот момент, когда код начинает читаться как последовательность шагов: распарсили → выбросили нераспарсившееся → дальше работаем с нормальными данными.

Сам контракт filterNotNull() как раз в этом и состоит: оставить только non-null элементы и сузить тип до non-null.

Комбинация «очистка → проверка» через mapNotNull + any

Теперь соберём мини-цепочку, которая реально полезна: «пользователь ввёл что-то, мы вытащили числа, и хотим понять, есть ли среди них большие суммы».

fun main() {
    val line = "10  200  x  50"
    val hasBig = line.split(" ")
        .mapNotNull { it.trim().toIntOrNull() }
        .any { it >= 100 }

    println(hasBig) // true
}

Заметьте, как хорошо сочетаются темы дня: сначала mapNotNull возвращает List<Int>, и только потом any работает с нормальными числами, без Int?.

5. Типичные ошибки при работе с mapNotNull и filterNotNull

Ошибка №1: использовать map { toIntOrNull() } и дальше вести себя так, будто это List<Int>.
Это происходит почти автоматически: вы глазами видите «числа», мозг радуется, а компилятор напоминает, что это Int?. На практике это лечится одним движением: если вы не хотите работать с nullable-элементами, превращайте map в mapNotNull (особенно в парсинге). И да, это ровно тот случай, для которого mapNotNull и задуман: «преобразование, которое может дать null».

Ошибка №2: ожидать, что filterNotNull() «почистит данные вообще», включая пустые строки, нули и пробелы.
filterNotNull() — не «санитар», а «вышибала строго по паспорту»: он выгоняет только null. Пустая строка — это строка, и она останется. Если вам нужно убрать пустые строки, это делается отдельным фильтром вроде filter { it.isNotEmpty() }. Смешивать эти понятия опасно: вы будете уверены, что данные чистые, а потом удивитесь, почему "" внезапно где-то пролезла.

Ошибка №3: писать map { ... }.filterNotNull() там, где логичнее mapNotNull { ... }, и тем самым ухудшать читаемость.
Технически это работает, но теряется смысл. mapNotNull читается как цельная операция: «преобразуй и выкинь null-результаты». Когда вы пишете map().filterNotNull(), читатель должен сначала понять, что вы вернули nullable-результаты, и только потом увидеть, что вы их чистите. Это не катастрофа, но в учебных и командных проектах «читаемость по смыслу» — половина победы.

Ошибка №4: пытаться сообщать об ошибках парсинга через println прямо внутри mapNotNull.
Иногда хочется так: «если не распарсилось — напечатаю предупреждение». И можно, но это уже побочный эффект внутри операции преобразования. Код становится менее предсказуемым (особенно если потом вы захотите переиспользовать этот пайплайн не в CLI, а, например, в тестах). Обычно лучше разделять: либо возвращать null как сигнал «пропусти», либо собирать ошибки отдельным способом. Если очень хочется — выносите в отдельную функцию, и уже там решайте, печатать или нет.

Ошибка №5: использовать toInt() вместо toIntOrNull() в «грязных» данных и надеяться, что всё будет хорошо.
toInt() в случае мусора кидает исключение NumberFormatException, а toIntOrNull() возвращает null, что идеально стыкуется с mapNotNull. Это не просто «стиль», это фундаментальная разница в поведении. Kotlin прямо показывает безопасный путь как альтернативу исключениям: toIntOrNull() вместо падения.

1
Задача
Kotlin SELF, 22 уровень, 3 лекция
Недоступна
Список гостей
Список гостей
1
Задача
Kotlin SELF, 22 уровень, 3 лекция
Недоступна
Очистка чисел
Очистка чисел
1
Задача
Kotlin SELF, 22 уровень, 3 лекция
Недоступна
Купоны магазина
Купоны магазина
1
Задача
Kotlin SELF, 22 уровень, 3 лекция
Недоступна
Позиции выдачи
Позиции выдачи
Комментарии (1)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Michael Уровень 28
7 июня 2026
И кто нам рассказывал про takeIf? (Купоны магазина)