Обработка исключений в Kotlin
При выполнении программы могут возникать ситуации, мешающие программе выполняться дальше. Такие ошибки обычно называют исключениями. Другими словами, возникает исключительная ситуация. В теории ее быть не должно, но по каким-то причинам она возникла. Программист должен уметь предвидеть такие ошибки и закладывать в программу логику их обработки.
Например, программа ожидает от пользователя число, а он вводит буквы. В таком случае в процессе выполнения возникнет исключение вида NumberFormatException
, и программа завершится.
В примере на скрине программа прерывается при попытке преобразовать строку в число с помощью функции toInt()
. До суммирования и вывода числа на экран дело не доходит.
Для обработки исключений в языках программирования предусмотрена специальная конструкция. В случае Kotlin это try-catch. Логика ее работы чем-то похожа на оператор if-else. В ветке try
мы пытаемся выполнить какое-то действие, которое потенциально может привести к выбросу исключения. Если этого не происходит, то тело try
выполняется полностью. Ветку catch
поток выполнения игнорирует.
Если же в try
происходит исключительная ситуация, поток выполнения программы тут же прерывает выполнение тела try
и уходит в ветку catch
, где это исключение обрабатывается. Другими словами, происходит перехват, ловля исключения. Программа не дает ему "выйти наружу".
Посмотрим как это работает в случае нашей программы.
fun main() { val a = readln() 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 = readln() 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 = readln() val b = readln() 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("Конец программы") } ...
В примере выше намерено опущена еще одна ветка catch
, чтобы показать, что finally
срабатывает во всех трех случаях: когда исключений нет, когда исключение обработано и даже когда не обработано. Если бы выражение println("Конец программы")
находилось за пределами инструкции try-catch-finally, то в случае необработанного исключения оно бы не выполнялось.
Ветка finally
необязательна только в случае наличия хотя бы одной ветки catch
. Если есть только try
без catch
, то блок finally
становится обязательным.
... try { c = a.toInt() / b.toInt() println(c) } finally { println("Конец программы") } ...
Практическая работа:
Доработайте программу из данного урока так, чтобы она не завершалась до тех пор, пока пользователь не введет два числа, и второе из них не будет нулем.
PDF-версия курса с ответами к практическим работам