JavaRush /Курсы /Kotlin SELF /finally и throw

finally и throw

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

1. finally: «уборка», которая случится в любом сценарии

Когда вы пишете программу, вы почти всегда мысленно ожидаете «идеальный мир»: пользователь вводит корректные данные, вычисления проходят, мы печатаем результат и красиво уходим со сцены под аплодисменты. Но реальный мир — это когда пользователь вводит -999, затем q, затем 2147483648, а потом обижается, что программа «не понимает простых вещей». Блок finally как раз про то, чтобы ваш код корректно завершал важные действия независимо от того, случилась ошибка или нет.

Формально, finally { ... } — это блок, который выполняется всегда: и если try отработал успешно, и если внутри try произошло исключение, и даже если исключение поймали в catch. Это классическая конструкция try/catch/finally, и Kotlin описывает finally как блок для cleanup — действий, которые нужно выполнить в любом случае.

Представьте аналогию: вы готовите на кухне. Неважно, получилось ли блюдо или вы случайно сожгли макароны — посуду мыть всё равно придётся. Вот finally — это «помыть посуду», а не «исправить макароны».

Мини-скелет try/catch/finally


fun main() {
    try {
        println("try: делаем что-то полезное")
        println(10 / 2)                 // 5
    } catch (e: ArithmeticException) {
        println("catch: ошибка деления")
    } finally {
        println("finally: я выполнюсь всегда")
    }
}

Если поменять 10 / 2 на 10 / 0, строка из catch выполнится, но строка из finally тоже выполнится.

try { ... } finally { ... } без catch

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

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

fun main() {
    println("Старт")

    try {
        val a = 10
        val b = 0
        println(a / b)                  // ArithmeticException
    } finally {
        println("Завершение: спасибо, что протестировали программу")
    }

    println("Финиш")                    // не выполнится
}

Обратите внимание на важную деталь: finally выполнится, но строка println("Финиш") — нет. То есть finally не «исцеляет» программу. Он просто гарантирует, что вы успеете сделать нужное завершающее действие.

finally и return: finally выполняется, но результат не “переписывает”

На первых порах finally может казаться магией: «Он всегда выполняется, значит он может всё поменять?» Нет: у finally другая роль. Kotlin отдельно подчёркивает, что finally всегда выполняется, но не меняет результат try/catch-выражения (то, что вернётся из try или catch, то и будет результатом).

Это важно, потому что вы скоро начнёте писать функции, которые возвращают значение из try/catch. И вам нужно понимать модель: finally — это «побочный завершающий ритуал», а не «ещё один способ вернуть значение».

fun divideOrMinusOne(a: Int, b: Int): Int {
    try {
        return a / b
    } catch (e: ArithmeticException) {
        return -1
    } finally {
        println("finally: попытка деления завершена")
    }
}

fun main() {
    println(divideOrMinusOne(10, 2))    // finally: попытка деления завершена
                                        // 5
    println(divideOrMinusOne(10, 0))    // finally: попытка деления завершена
                                        // -1
}

Если вы впервые видите return внутри try/catch/finally, не пугайтесь. Логика простая: как только найдено, что вернуть, Kotlin всё равно выполняет finally перед выходом из функции.

2. throw: как честно сказать «так нельзя»

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

Для этого есть ключевое слово throw: оно явно выбрасывает исключение. Kotlin показывает это так же прямо, как в других языках: throw IllegalArgumentException("...").

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

Пример: функция принимает только положительное число

fun parsePositiveInt(text: String): Int {
    val n = text.toInt()
    if (n <= 0) {
        throw IllegalArgumentException("Ожидалось число > 0, но пришло: $n")
    }
    return n
}

Здесь мы не «ловим» ошибку. Мы формируем контракт функции: она возвращает Int, но только если число положительное. Если нет — это не “обычный сценарий”, а нарушение правил использования функции.

Важный нюанс: throw — выражение типа Nothing

Это звучит страшнее, чем есть на самом деле. Тип Nothing вы уже встречали раньше: он означает «это выражение никогда не возвращает значение». Kotlin-справка показывает классический паттерн, где throw удобно использовать как «жёсткий fallback» в Elvis-операторе ?:.

fun fail(message: String): Nothing {
    throw IllegalArgumentException(message)
}

fun main() {
    val name: String? = null

    val nonNullName = name ?: fail("Имя обязательно")
    println(nonNullName)                // сюда не дойдём
}

Пока воспринимайте это как приятный бонус: компилятор понимает, что после fail(...) выполнение не продолжается, и поэтому типы «сходятся».

Как выбрать исключение и написать сообщение

Когда вы впервые узнаёте про throw, появляется соблазн: «О! Буду бросать исключения вообще везде». Не спешите превращать код в минное поле. throw хорош там, где вы хотите защитить смысл функции: если входные данные не подходят, лучше остановиться и сказать об этом явно.

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

Сообщение исключения — это ваша записка самому себе (и будущему вам). Хорошее сообщение обычно содержит две части: что ожидалось и что пришло. Тогда при падении вам не придётся гадать, почему программа решила завершиться.

fun divideStrict(a: Int, b: Int): Int {
    if (b == 0) {
        throw IllegalArgumentException("Делитель не может быть 0 (a=$a, b=$b)")
    }
    return a / b
}

Да, можно было бы просто попытаться поделить и поймать ArithmeticException. Но иногда лучше сделать проверку и выбросить своё объяснимое исключение. Тут нет единственного правильного ответа — важно, чтобы поведение было понятным.

3. Мини-приложение: деление с понятным поведением

Сейчас будет самая практичная часть: мы соберём маленькое консольное приложение, которое делает одно действие (деление), но делает это по-взрослому: аккуратно читает ввод, проверяет правила, сообщает человеку, что не так, и всегда печатает финальную строку в конце попытки. Это тот стиль, который позже очень пригодится, когда вы начнёте писать функции ввода и валидации (да, это будет отдельный уровень курса — сегодня только фундамент).

Шаг 1: читаем число или null

fun readIntOrNull(prompt: String): Int? {
    print(prompt)
    val raw = readln().trim()
    return raw.toIntOrNull()
}

Здесь ничего волшебного: trim() убирает лишние пробелы, а toIntOrNull() либо возвращает Int, либо null.

Шаг 2: строгое деление с throw

fun divideStrict(a: Int, b: Int): Int {
    if (b == 0) throw IllegalArgumentException("Делить на 0 нельзя")
    return a / b
}

Это наш «контракт»: делитель 0 — запрещён правилами, поэтому мы явно говорим «так нельзя».

Шаг 3: main — ловим ожидаемую ошибку и всегда завершаем

fun main() {
    val a = readIntOrNull("Введите a: ")
    val b = readIntOrNull("Введите b: ")

    try {
        if (a == null || b == null) throw IllegalArgumentException("Нужно ввести два целых числа")
        val result = divideStrict(a, b)
        println("a / b = $result")              // например: a / b = 5
    } catch (e: IllegalArgumentException) {
        println("Ошибка ввода/правил: ${e.message}")
    } finally {
        println("Готово: попытка вычисления завершена")
    }
}

Что здесь важно заметить (и это прям ключевой навык уровня):

try содержит только то, что реально может «сломаться» в рамках нашей логики: проверка правил, вызов divideStrict(...), печать результата. Если всё прошло, catch пропускается, но finally всё равно выполняется. Если мы бросили IllegalArgumentException (сами или из divideStrict), управление прыгает в catch, а затем всё равно выполняется finally.

Схема потока выполнения

Когда вы только начинаете, try/catch/finally легко превратить в кашу в голове: «куда прыгает управление и что выполняется?». Тут помогает простая блок-схема. Представьте, что программа — это поезд, а исключение — это экстренный тормоз.

flowchart TD
    A[Входим в try] --> B{В try случилось исключение?}
    B -- нет --> C[Выполняем код try до конца]
    B -- да --> D[Прыгаем в catch подходящего типа]
    C --> E[Выполняем finally]
    D --> E[Выполняем finally]
    E --> F{Исключение обработано?}
    F -- да --> G[Продолжаем после try/catch/finally]
    F -- нет --> H["Исключение 'улетает' выше, программа может упасть"]

Эта схема не про «внутренности JVM», а про вашу ежедневную практику: понимать, почему следующая строка не выполняется, но finally выполняется.

4. Типичные ошибки при работе с finally и throw

Ошибка №1: пытаться “обрабатывать” ошибку в finally.
Иногда новичок думает: раз finally выполняется всегда, то туда надо положить «логику спасения». Но finally для другого: там не чинят проблему, там делают уборку. Если вы в finally начинаете разбирать ввод или менять важные данные, код становится непредсказуемым: ошибка уже произошла, а вы добавляете ещё один слой сюрпризов.

Ошибка №2: писать в finally код, который сам может упасть.
Это звучит как шутка, но на практике встречается постоянно: в finally печатают что-то, обращаются к переменной, которая могла не инициализироваться, или делают деление «для лога». В итоге вы теряете исходную причину ошибки, потому что поверх неё накладывается новая. Хороший finally обычно короткий, простой и максимально надёжный.

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

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

Ошибка №5: думать, что finally “продолжит программу” после падения.
finally выполнится, да. Но если исключение не поймано (или вы используете try/finally без catch), то после finally выполнение не превращается обратно в нормальный сценарий: исключение всё равно уйдёт выше. Kotlin показывает, что finally гарантирует выполнение завершающего кода, но не обещает, что программа пойдёт дальше, если ошибка не обработана.

1
Задача
Kotlin SELF, 12 уровень, 3 лекция
Недоступна
Отчёт с уборкой
Отчёт с уборкой
1
Задача
Kotlin SELF, 12 уровень, 3 лекция
Недоступна
Ритуал очистки
Ритуал очистки
1
Задача
Kotlin SELF, 12 уровень, 3 лекция
Недоступна
Скидка на входе
Скидка на входе
1
Задача
Kotlin SELF, 12 уровень, 3 лекция
Недоступна
Счётчик делений
Счётчик делений
Комментарии (1)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Серега PARADOX Уровень 21
30 марта 2026
Шаг 3 строка 10 в комментах привести бы напечатанный текст программы для точного понимания. А то код есть, а результат не добавили