Обработка исключений в Kotlin

При выполнении программы могут возникать ситуации, мешающие программе выполняться дальше. Такие ошибки обычно называют исключениями. Другими словами, возникает исключительная ситуация. В теории ее быть не должно, но по каким-то причинам она возникла. Программист должен уметь предвидеть такие ошибки и закладывать в программу логику их обработки.

Например, программа ожидает от пользователя число, а он вводит буквы. В таком случае в процессе выполнения возникнет исключение вида NumberFormatException, и программа завершится.

Выброс исключения NumberFormatException

В примере на скрине программа прерывается при попытке преобразовать строку в число с помощью функции toInt(). До суммирования и вывода числа на экран дело не доходит.

Для обработки исключений в языках программирования предусмотрена специальная конструкция. В случае Kotlin это try-catch. Логика ее работы чем-то похожа на оператор if-else. В ветке try мы пытаемся выполнить какое-то действие, которое потенциально может привести к выбросу исключения. Если этого не происходит, то тело try выполняется полностью. Ветку catch поток выполнения игнорирует.

Если же в try происходит исключительная ситуация, поток выполнения программы тут же прерывает выполнение тела try и уходит в ветку catch, где это исключение обрабатывается. Другими словами, происходит перехват, ловля исключения. Программа не дает ему "выйти наружу".

Посмотрим как это работает в случае нашей программы.

fun main() {
    val a = readLine()!!
    var b: Int
 
    try {
        b = a.toInt() + 10
    }
    catch (e: Exception) {
        b = 10
    }
 
    println("b = $b")
}

Если выражение a.toInt() выбросит исключение, то до прибавления числа 10 и присвоения результата переменной b дело не дойдет. Произойдет переход в ветку catch, где b будет просто присвоено число 10.

Поскольку try-catch, как и if-else, является не только инструкцией, но и выражением, программу можно привести к такому виду:

fun main() {
    val a = readLine()!!
    val b = try {
        a.toInt() + 10
    }
    catch (e: Exception) {
        10
    }
 
    println("b = $b")
}

Существует большое количество исключений, у каждого из них есть свой тип-класс. Однако все эти классы объединены в общий класс – Exception. Если в заголовке catch в круглых скобках указана переменная типа Exception, данный блок catch будет перехватывать все виды исключений. Однако более предпочтительным считается указание конкретного типа исключений, который обрабатывает определенный блок catch. Для других типов исключений, если они возможны, добавляют свои блоки catch.

Другими словами, инструкцию try-catch можно расширить до try-catch-catch-… . Причем более общие классы исключений должны находится в более удаленных от try блоках catch. Рассмотрим программу, которая потенциально может генерировать два типа исключений. Первое – из-за неправильного ввода, второе – при попытке деления на 0 (ArithmeticException).

fun main() {
    val a = readLine()!!
    val b = readLine()!!
    val c: Int
 
    try {
        c = a.toInt() / b.toInt()
        println(c)
    }
    catch (e: NumberFormatException) {
        println("Надо вводить только числа")
    }
    catch (e: Exception) {
        println(e.toString())
    }
}

Выражение c = a.toInt() / b.toInt() составное. В нем происходят сначала операции преобразования строк в числа, затем деление и в конце присваивание. Выполнение этого выражение прервется сразу, как только возникнет любое исключение. Если преобразование к целочисленному типу не удастся, то поток выполнения уйдет в первую ветку catch, которая обрабатывает узкий класс исключений – NubmerFormatException. Вторая ветка catch будет проигнорирована.

Если в теле try исключение будет выброшено операцией деления, то оно будет обработано второй веткой catch, которая обрабатывает все исключения.

Если поменять местами блоки catch, несмотря на то, что это не будет синтаксической ошибкой, логика программы будет неверной.

...
    try {
        c = a.toInt() / b.toInt()
        println(c)
    }
    catch (e: Exception) {
        println(e.toString())
    }
    catch (e: NumberFormatException) {
        println("Надо вводить только числа")
    }
...

При таком варианте последнее catch не сработает никогда. Все ошибки будут отлавливаться более общим классом Exception. Пример выполнения программы при вводе строки вместо числа:

5
hello
java.lang.NumberFormatException: For input string: "hello"

Если бы сработала вторая ветка catch, мы бы увидели надпись "Надо вводить только числа". Мы же видим преобразованную к строковому типу информацию об исключении – e.toString().

С другой стороны, мы можем конкретизировать и обработку деления на ноль. В этом случае порядок веток catch не важен:

... 
   try {
        c = a.toInt() / b.toInt()
        println(c)
    }
    catch (e: ArithmeticException) {
        println("Делить на ноль нельзя")
    }
    catch (e: NumberFormatException) {
        println("Надо вводить только числа")
    }
...

Пример выполнения при вводе строки вместо числа:

hi
10
Надо вводить только числа

Пример выполнения, если второе число – ноль:

12
0
Делить на ноль нельзя

В обработчике исключений try-catch также может быть ветка finally, которая всегда идет последней. Тело finally содержит код, который выполняется всегда, независимо от возникших обстоятельств. Было ли исключение, не было ли его и весь try выполнился полностью, не важно; finally все равно сработает.

...
    try {
        c = a.toInt() / b.toInt()
        println(c)
    }
    catch (e: ArithmeticException) {
        println("Делить на ноль нельзя")
    }
    finally {
        println("Конец программы")
    }
...

Постобработка в ветке finally

В примере выше намерено опущена еще одна ветка catch, чтобы показать, что finally срабатывает во всех трех случаях: когда исключений нет, когда исключение обработано и даже когда не обработано. Если бы выражение println("Конец программы") находилось за пределами инструкции try-catch-finally, то в случае необработанного исключения оно бы не выполнялось.

Ветка finally необязательна только в случае наличия хотя бы одной ветки catch. Если есть только try без catch, то блок finally становится обязательным.

...
    try {
        c = a.toInt() / b.toInt()
        println(c)
    }
    finally {
        println("Конец программы")
    }
...

Практическая работа:

Доработайте программу из данного урока так, чтобы она не завершалась до тех пор, пока пользователь не введет два числа, и второе из них не будет нулем.