JavaRush /Курсы /Kotlin SELF /Знакомство с исключениями

Знакомство с исключениями

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

1. Ошибки компиляции и ошибки выполнения

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

Для наглядности удобно сравнить эти вещи в таблице:

Что это Когда возникает Что вы видите Пример
Ошибка компиляции До запуска программы IDE ругается, kotlinc не собирает
val x: Int = "привет"
Исключение (runtime exception) Во время выполнения Программа падает, печатается сообщение и stack trace
println(10 / 0)

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

В Kotlin исключения представлены объектами, которые являются наследниками Throwable. В общих чертах: ThrowableException / Error. Обычно в прикладном коде мы сталкиваемся с Exception, а Error — это, скорее, история про серьёзные проблемы уровня среды выполнения (например, нехватка памяти).

Что такое исключение

Представьте, что ваша программа — это поезд, который едет по рельсам строк кода. Обычный поток выполнения — поезд едет по маршруту: строка 1, строка 2, строка 3… Исключение — это ситуация, когда на рельсах внезапно оказался бетонный блок: дальше ехать нельзя, и поезд не будет «аккуратно» переезжать препятствие. Он останавливается, и управление передаётся специальному механизму обработки ошибок.

Формально (и очень важно это запомнить): когда выбрасывается исключение, нормальное выполнение прерывается. Это буквально означает: «следующая строка после проблемной не выполнится», если исключение не перехвачено.

Мини-пример — классика жанра «деление на ноль»:

fun main() {
    println("start")
    val a = 10
    val b = 0
    println(a / b)        // ArithmeticException
    println("end")        // не выполнится
}

Обратите внимание: строка println("end") выглядит невинно, но она не будет выполнена. Потому что программа «вылетела» из обычного маршрута на строке a / b.

2. Исключение прерывает выполнение и не даёт «идти дальше»

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

Когда исключение появляется, текущая функция заканчивается не по return, а аварийно. И дальше Kotlin/JVM пытается найти место, которое готово это исключение обработать. Если такого места нет — программа завершается, а вы видите сообщение об ошибке и стек вызовов.

Ещё одна важная деталь: исключение может возникнуть не там, где вы написали «опасный» код, а «глубоко» внутри функций, которые вы вызвали. Например, вы вызываете toInt(), а внутри него Kotlin пытается распарсить строку и, если не получается, выбрасывает NumberFormatException. Это исключение «вылетает» наружу к вашему коду.

Пример:

fun main() {
    val text = "12a"
    val n = text.toInt()  // NumberFormatException
    println("n = $n")     // не выполнится
}

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

3. Откуда вообще берутся исключения

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

Арифметика: деление на ноль

С арифметикой всё интуитивно: некоторые операции математически не определены. JVM не будет «догадываться», что вы имели в виду, и выбросит исключение.

fun main() {
    val total = 100
    val days = 0
    val perDay = total / days
    println(perDay) // сюда не дойдём
}

Преобразование строки в число: «это не число»

Вы уже знаете безопасный путь toIntOrNull(). Но toInt() — «строгий» вариант. Если строка не является корректным целым числом, возникает NumberFormatException.

fun main() {
    val raw = "сорок два"
    val n = raw.toInt()      // NumberFormatException
    println(n)
}

Индексы массива: вылезли за границы

Массивы фиксированного размера, и индексы должны быть в диапазоне 0..lastIndex. Если нет — вы получите исключение времени выполнения (на JVM это часто ArrayIndexOutOfBoundsException).

fun main() {
    val xs = intArrayOf(10, 20, 30)
    println(xs[2])  // 30
    println(xs[5])  // ArrayIndexOutOfBoundsException
}

Психологическая ловушка здесь такая: «ну индекс же число, я же его распарсил!». Да, число. Но не любое число — допустимое. И исключение как раз сообщает: «правило нарушено».

4. Иерархия исключений: Throwable, Exception, Error

Когда вы видите сообщение об ошибке, там почти всегда есть имя типа исключения. Чтобы не воспринимать это как набор случайных заклинаний, полезно понимать верхний уровень иерархии.

Корень иерархии — Throwable. У него есть два важных «потомка первого уровня»: Exception и Error. Идея такая: Exception — это то, что в принципе может быть частью нормальной жизни программы (ошибка ввода, недоступность ресурса и т.п.), а Error — это обычно серьёзные проблемы среды, которые «чинить» на уровне вашей логики бессмысленно (например, OutOfMemoryError).

На данном этапе нам достаточно такой схемы:

flowchart TD
    T[Throwable]
    E[Exception]
    ER[Error]
    T --> E
    T --> ER
    E --> NFE[NumberFormatException]
    E --> AE[ArithmeticException]
    E --> AIOOBE[ArrayIndexOutOfBoundsException]

Важно: это не означает, что вы обязаны знать всю иерархию наизусть. Но когда вы видите ArithmeticException, вы должны думать: «ага, это Exception, а значит ошибка уровня выполнения моего сценария».

Причина и место: имя исключения и stack trace

Когда программа падает, у новичка часто ощущение: «сломалось где-то». Но механизм исключений специально устроен так, чтобы дать вам две подсказки: что случилось (причина) и где случилось (место).

Причина чаще всего зашита в названии типа и сообщении. Например, NumberFormatException буквально означает: «строка не подходит по формату числа». Место обычно видно в stack trace — это цепочка вызовов функций, которая привела к ошибке. Kotlin/JVM при необработанном исключении печатает stack trace автоматически.

Мини-пример, который специально создаёт ошибку:

fun main() {
    throw ArithmeticException("Пример исключения")
}

Примерный вывод (вид может отличаться, но смысл такой же):

Exception in thread "main" java.lang.ArithmeticException: Пример исключения
    at MainKt.main(Main.kt:2)

Пока что не нужно уметь читать stack trace идеально (мы будем учиться делать это отдельно). Но уже сейчас полезно знать: если программа упала, она обычно оставила «квитанцию» о том, где именно.

5. Практический пример

Чтобы исключения не оставались абстракцией, давайте продолжим писать маленькое консольное приложение. Пусть оно называется BudgetBuddy: пользователь вводит расходы за несколько дней, а программа считает сумму и «среднее за день».

Мы пока пишем максимально просто, без обработки исключений (это будет следующая лекция). Наша цель здесь — увидеть, как именно ломается поток выполнения.

Наивная версия: «всё по плану»

Начнём с простого сценария: читаем количество дней, затем читаем расходы по дням в массив, потом считаем сумму.

fun main() {
    print("Сколько дней? ")
    val days = readln().toInt()

    val expenses = IntArray(days)

    for (i in 0 until days) {
        print("Расход за день ${i + 1}: ")
        expenses[i] = readln().toInt()
    }

    var total = 0
    for (x in expenses) total += x

    println("Итого: $total")
}

Если пользователь вводит нормальные числа, всё хорошо. Но «нормальные числа» — это несбыточная мечта.

Сценарий падения: пользователь ввёл не число

Пусть пользователь вводит так:

  • «Сколько дней?» → 3
  • «Расход за день 1» → 100
  • «Расход за день 2» → сто (или 100р)

На строке readln().toInt() произойдёт NumberFormatException, и программа завершится. Причём это произойдёт внутри цикла, а значит мы даже не дойдём ни до суммы, ни до финального println("Итого...").

Сценарий падения: пользователь ввёл 0 дней (а мы захотели среднее)

Добавим среднее значение. Наивно:

fun main() {
    print("Сколько дней? ")
    val days = readln().toInt()

    print("Сумма расходов: ")
    val total = readln().toInt()

    val avg = total / days
    println("Среднее за день: $avg")
}

Если days = 0, на строке total / days будет ArithmeticException. И снова: строка println(...) не выполнится.

Что здесь особенно коварно: даже если days успешно распарсился (то есть это действительно число), оно всё равно может быть недопустимым для вашей логики. Исключения часто возникают не на распарсивании, а на использовании значения.

Сценарий падения: неправильная работа с индексом

Представим, что мы хотим показать «последний введённый расход», но ошиблись и взяли индекс days, а не days - 1. (Это боль, которую пережили поколения программистов, и традиция будет продолжена.)

fun main() {
    val expenses = intArrayOf(100, 200, 300)
    val days = expenses.size

    println(expenses[days]) // days == 3, а индексы 0..2
}

Это даст ArrayIndexOutOfBoundsException. Программа снова завершится аварийно.

Что происходит при исключении: мини-схема

Чтобы закрепить модель, полезно представить поведение программы в виде схемы. Наша программа выполняет строки, пока всё хорошо. Если строка выбрасывает исключение, происходит «аварийный выход» из текущей функции и подъём наверх по вызовам.

flowchart TD
    A[Выполняем строку за строкой] --> B{Произошла ошибка?}
    B -->|нет| A
    B -->|да: исключение| C[Прерываем текущую функцию]
    C --> D[Ищем обработчик выше по стеку]
    D --> E{Нашли обработчик?}
    E -->|нет| F[Печатаем stack trace и завершаем программу]
    E -->|да| G[Передаём управление обработчику]

Сегодня вам достаточно запомнить ветку «не нашли обработчик → программа завершилась». Про ветку «нашли обработчик» мы будем подробно говорить в следующей лекции, когда появится try/catch.

6. Типичные ошибки

Ошибка №1: путать ошибку компиляции и исключение времени выполнения.
Если IDE подчёркивает код ещё до запуска — это компиляция, программа даже не стартует. Если же программа запустилась, что-то успела вывести, а потом внезапно «упала» и показала длинный текст — это исключение, то есть проблема времени выполнения.

Ошибка №2: ожидать, что код после «проблемной строки» всё равно выполнится.
Это прям самое частое. Кажется, что println("end") обязан напечататься, ведь он «после» деления. Но исключение ломает линейность: выполнение прерывается. В голове нужно переключить тумблер: «исключение = аварийный выход».

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

Ошибка №4: игнорировать название исключения и читать stack trace как «страшную простыню».
Даже без навыков отладки, название исключения часто очень информативно: NumberFormatException — не число, ArithmeticException — арифметика, ArrayIndexOutOfBoundsException — индекс мимо кассы. Kotlin/JVM специально даёт эти подсказки.

Ошибка №5: думать, что исключение — это «конец света», и поэтому лучше «не трогать».
На самом деле исключения — нормальный рабочий механизм. Проблема не в их существовании, а в том, что мы пока не умеем их грамотно обрабатывать и превращать в понятное поведение программы. Этим как раз и займёмся дальше: научимся ловить ожидаемые исключения и продолжать работу, не превращая консоль в драмтеатр.

1
Задача
Kotlin SELF, 12 уровень, 1 лекция
Недоступна
Касса на двоих
Касса на двоих
1
Задача
Kotlin SELF, 12 уровень, 1 лекция
Недоступна
Сломанный купон
Сломанный купон
1
Задача
Kotlin SELF, 12 уровень, 1 лекция
Недоступна
Полка на складе
Полка на складе
1
Задача
Kotlin SELF, 12 уровень, 1 лекция
Недоступна
Лифт сбоев
Лифт сбоев
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ