371 lines
16 KiB
Markdown
371 lines
16 KiB
Markdown
---
|
||
title: Система исключений
|
||
description: Иерархия Throwable, try как выражение, отсутствие checked exceptions и правильная обработка
|
||
tags:
|
||
- kotlin
|
||
- exceptions
|
||
---
|
||
|
||
Все исключения в Kotlin **непроверяемые**. Компилятор не требует ни объявлять, ни перехватывать их.
|
||
|
||
```kotlin
|
||
fun parse(s: String): Int = s.toInt() // может бросить, но throws не нужен
|
||
|
||
fun main() {
|
||
println(parse("42")) // 42
|
||
try {
|
||
parse("abc")
|
||
} catch (e: NumberFormatException) {
|
||
println("не число: ${e.message}") // не число: For input string: "abc"
|
||
}
|
||
}
|
||
```
|
||
|
||
## Иерархия
|
||
```mermaid
|
||
graph TD
|
||
T["Throwable"] --> E["Exception"]
|
||
T --> R["Error"]
|
||
E --> RE["RuntimeException"]
|
||
E --> IOE["IOException"]
|
||
RE --> NPE["NullPointerException"]
|
||
RE --> IAE["IllegalArgumentException"]
|
||
RE --> ISE["IllegalStateException"]
|
||
RE --> CCE["ClassCastException"]
|
||
R --> OOM["OutOfMemoryError"]
|
||
R --> SOE["StackOverflowError"]
|
||
```
|
||
Иерархия та же, что в Java, - это классы JDK. Разница только в том, что Kotlin не делит их на проверяемые и непроверяемые.
|
||
|
||
| Класс | Смысл | Перехватывать |
|
||
|---|---|---|
|
||
| `Error` | сбой среды: нет памяти, переполнен стек | нет |
|
||
| `RuntimeException` | ошибка программиста: неверный аргумент, состояние | обычно нет, лучше исправить код |
|
||
| прочие `Exception` | внешний сбой: сеть, файл, БД | да |
|
||
|
||
## try как выражение
|
||
`try` возвращает значение - это важное отличие от Java:
|
||
```kotlin
|
||
fun parseOrZero(s: String): Int = try {
|
||
s.toInt()
|
||
} catch (e: NumberFormatException) {
|
||
0
|
||
}
|
||
|
||
fun main() {
|
||
println(parseOrZero("42")) // 42
|
||
println(parseOrZero("abc")) // 0
|
||
}
|
||
```
|
||
Результат - последнее выражение блока `try` или соответствующего `catch`. Блок `finally` на результат **не влияет**:
|
||
```kotlin
|
||
fun main() {
|
||
val result = try {
|
||
"из try"
|
||
} finally {
|
||
"из finally" // игнорируется
|
||
}
|
||
println(result) // из try
|
||
}
|
||
```
|
||
|
||
## throw - тоже выражение
|
||
Тип `throw` - [Nothing](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Any,%20Unit,%20Nothing.md), поэтому он подходит в любую позицию:
|
||
```kotlin
|
||
fun findUser(id: Int): String =
|
||
if (id > 0) "пользователь $id"
|
||
else throw IllegalArgumentException("неверный id: $id")
|
||
|
||
fun main() {
|
||
println(findUser(1)) // пользователь 1
|
||
val name = findUser(1).takeIf { it.isNotEmpty() } ?: throw IllegalStateException("пусто")
|
||
println(name) // пользователь 1
|
||
}
|
||
```
|
||
Это позволяет писать `?: throw ...` и `?: error(...)` в одном выражении.
|
||
|
||
## Перехват
|
||
```kotlin
|
||
fun risky(mode: Int): String = when (mode) {
|
||
1 -> throw IllegalArgumentException("плохой аргумент")
|
||
2 -> throw IllegalStateException("плохое состояние")
|
||
else -> "ок"
|
||
}
|
||
|
||
fun handle(mode: Int): String = try {
|
||
risky(mode)
|
||
} catch (e: IllegalArgumentException) {
|
||
"аргумент: ${e.message}"
|
||
} catch (e: IllegalStateException) {
|
||
"состояние: ${e.message}"
|
||
} catch (e: Exception) {
|
||
"прочее: ${e.message}"
|
||
} finally {
|
||
println("finally выполняется всегда")
|
||
}
|
||
|
||
fun main() {
|
||
println(handle(1)) // аргумент: плохой аргумент
|
||
println(handle(3)) // ок
|
||
}
|
||
```
|
||
Блоки `catch` проверяются по порядку, поэтому конкретные типы идут раньше общих. `catch (e: Exception)` первым сделал бы остальные недостижимыми.
|
||
|
||
Несколько типов в одном блоке Kotlin не поддерживает - аналога Java `catch (A | B e)` нет. Варианты:
|
||
```kotlin
|
||
fun main() {
|
||
try {
|
||
throw IllegalStateException("сбой")
|
||
} catch (e: RuntimeException) { // общий супертип
|
||
println("перехвачено: ${e::class.simpleName}")
|
||
}
|
||
|
||
try {
|
||
throw NumberFormatException("плохое число")
|
||
} catch (e: Exception) {
|
||
when (e) { // или when по типу
|
||
is NumberFormatException, is IllegalStateException ->
|
||
println("ожидаемая ошибка: ${e.message}")
|
||
else -> throw e // остальное пробрасываем
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
## Свои исключения
|
||
```kotlin
|
||
class InsufficientFundsException(
|
||
val required: Long,
|
||
val available: Long,
|
||
) : Exception("нужно $required, доступно $available")
|
||
|
||
class ValidationException(message: String, cause: Throwable? = null) : Exception(message, cause)
|
||
|
||
fun withdraw(balance: Long, amount: Long): Long {
|
||
if (amount > balance) throw InsufficientFundsException(amount, balance)
|
||
return balance - amount
|
||
}
|
||
|
||
fun main() {
|
||
println(withdraw(100, 30)) // 70
|
||
try {
|
||
withdraw(100, 500)
|
||
} catch (e: InsufficientFundsException) {
|
||
println("не хватает ${e.required - e.available}") // не хватает 400
|
||
}
|
||
}
|
||
```
|
||
Своё исключение полезно, когда вызывающему нужно различить ситуацию и получить данные о ней. Для «ошибки программиста» хватает стандартных `IllegalArgumentException` и `IllegalStateException`.
|
||
|
||
## Цепочка причин
|
||
```kotlin
|
||
class RepositoryException(message: String, cause: Throwable) : Exception(message, cause)
|
||
|
||
fun loadUser(id: String): String = try {
|
||
"пользователь ${id.toInt()}"
|
||
} catch (e: NumberFormatException) {
|
||
throw RepositoryException("не удалось загрузить '$id'", e) // сохраняем причину
|
||
}
|
||
|
||
fun main() {
|
||
try {
|
||
loadUser("abc")
|
||
} catch (e: RepositoryException) {
|
||
println(e.message) // не удалось загрузить 'abc'
|
||
println("причина: ${e.cause?.message}") // причина: For input string: "abc"
|
||
}
|
||
}
|
||
```
|
||
Передавать `cause` обязательно: без него теряется исходный стектрейс и причина сбоя.
|
||
|
||
## use - автоматическое закрытие
|
||
Аналог try-with-resources:
|
||
```kotlin
|
||
import java.io.StringReader
|
||
|
||
fun main() {
|
||
val content = StringReader("данные").use { reader ->
|
||
reader.readText() // close() вызовется автоматически
|
||
}
|
||
println(content) // данные
|
||
}
|
||
```
|
||
`use` - функция-расширение для `Closeable`, она закрывает ресурс и при исключении. Это предпочтительный способ: `finally` с ручным `close()` легко написать неверно.
|
||
|
||
```kotlin
|
||
class Resource(val name: String) : AutoCloseable {
|
||
fun work() = "работаю с $name"
|
||
override fun close() = println("$name закрыт")
|
||
}
|
||
|
||
fun main() {
|
||
val result = Resource("файл").use { it.work() }
|
||
println(result)
|
||
// файл закрыт
|
||
// работаю с файл
|
||
|
||
try {
|
||
Resource("другой").use { throw IllegalStateException("сбой") }
|
||
} catch (e: IllegalStateException) {
|
||
println("ресурс всё равно закрыт: ${e.message}")
|
||
}
|
||
}
|
||
```
|
||
|
||
## @Throws для Java
|
||
Kotlin не объявляет исключения, но Java-клиенту это может быть нужно:
|
||
```kotlin
|
||
import java.io.IOException
|
||
|
||
@Throws(IOException::class)
|
||
fun readFile(path: String): String {
|
||
if (path.isEmpty()) throw IOException("пустой путь")
|
||
return "содержимое $path"
|
||
}
|
||
|
||
fun main() { println(readFile("a.txt")) } // содержимое a.txt
|
||
```
|
||
Без `@Throws` Java-код не сможет перехватить проверяемое исключение - компилятор Java сочтёт, что метод его не бросает. Подробнее: [Kotlin из Java](../Взаимодействие%20с%20Java/Kotlin%20из%20Java.md)
|
||
|
||
## Чего не делать
|
||
```kotlin
|
||
fun main() {
|
||
// проглатывание: ошибка исчезает
|
||
try {
|
||
error("сбой")
|
||
} catch (e: Exception) {
|
||
// ничего — худшая практика
|
||
}
|
||
|
||
// перехват Throwable: ловит OutOfMemoryError и StackOverflowError
|
||
try {
|
||
error("сбой")
|
||
} catch (e: Exception) { // не Throwable
|
||
println("перехвачено корректно")
|
||
}
|
||
|
||
// исключение как управление потоком
|
||
val items = listOf(1, 2, 3)
|
||
println(items.getOrNull(10) ?: "нет элемента") // вместо try/catch
|
||
}
|
||
```
|
||
Правила:
|
||
- не перехватывать, если не знаете, что делать с ошибкой - пусть идёт выше
|
||
- не перехватывать `Throwable` и `Error`
|
||
- не использовать исключения для ожидаемых ситуаций - для них есть `null`, `Result` или sealed-типы
|
||
- всегда логировать или пробрасывать; пустой `catch` скрывает причину сбоя
|
||
|
||
Альтернативы исключениям: [Result и runCatching](Result%20и%20runCatching.md)
|
||
|
||
## Исключения в корутинах
|
||
Отмена корутины реализована через `CancellationException`, и перехватывать её нельзя:
|
||
```kotlin
|
||
// Требует kotlinx-coroutines-core.
|
||
// suspend fun work() {
|
||
// try {
|
||
// delay(1000)
|
||
// } catch (e: CancellationException) {
|
||
// throw e // обязательно пробросить!
|
||
// } catch (e: Exception) {
|
||
// println("настоящая ошибка")
|
||
// }
|
||
// }
|
||
```
|
||
`catch (e: Exception)` перехватит и отмену, из-за чего корутина продолжит работу после отмены. Подробнее: [Исключения в корутинах](../Корутины/Исключения%20в%20корутинах.md)
|
||
|
||
> [!info] Отличия от Java
|
||
> | Java | Kotlin |
|
||
> |---|---|
|
||
> | checked exceptions | **нет**, все непроверяемые |
|
||
> | `throws IOException` в сигнатуре | `@Throws` только для Java-клиентов |
|
||
> | `try` - инструкция | `try` - выражение |
|
||
> | `catch (A \| B e)` | нет аналога, нужен `when` |
|
||
> | try-with-resources | `use { }` |
|
||
> | `throw` - инструкция | `throw` - выражение типа `Nothing` |
|
||
>
|
||
> **Отсутствие checked exceptions - самое спорное решение.** Аргументы против них, которые приводят авторы Kotlin:
|
||
>
|
||
> 1. **Они не масштабируются.** Сигнатура с пятью `throws` нечитаема, а обобщённый код (`map`, `forEach`) не может объявить исключения вызываемой лямбды - из-за этого Java-стримы несовместимы с проверяемыми исключениями.
|
||
> 2. **На практике их проглатывают.** Самый частый способ «обработать» обязательное исключение - пустой `catch`, то есть защита превращается в сокрытие ошибки.
|
||
> 3. **Обёртывание ради компиляции.** Код вроде `catch (IOException e) { throw new RuntimeException(e); }` встречается в любом крупном Java-проекте.
|
||
>
|
||
> Цена отказа - сигнатура больше не документирует ошибки. Это компенсируют KDoc и типы вроде `Result`, но компилятор уже не помогает.
|
||
>
|
||
> **`try` как выражение убирает `var`:**
|
||
> ```java
|
||
> int value; // Java: нужна изменяемая переменная
|
||
> try { value = Integer.parseInt(s); }
|
||
> catch (NumberFormatException e) { value = 0; }
|
||
> ```
|
||
> ```kotlin
|
||
> fun parseOrZero(s: String): Int =
|
||
> try { s.toInt() } catch (e: NumberFormatException) { 0 } // val вместо var
|
||
>
|
||
> fun main() { println(parseOrZero("abc")) } // 0
|
||
> ```
|
||
>
|
||
> **`use` вместо try-with-resources** - обычная функция, а не синтаксис. Поэтому её можно применить к своему типу, добавив реализацию `Closeable`.
|
||
>
|
||
> Сравнить: [Система исключений в Java](../../Программирование%20на%20Java/Исключения/Система%20исключений.md)
|
||
|
||
> [!warning] Подводные камни
|
||
> **Сигнатура не говорит об исключениях.** Единственные источники информации - документация и чтение кода. Для важных случаев указывайте в KDoc:
|
||
> ```kotlin
|
||
> /**
|
||
> * @throws IllegalArgumentException если id не положительный
|
||
> */
|
||
> fun load(id: Int): String {
|
||
> require(id > 0) { "id должен быть положительным" }
|
||
> return "данные $id"
|
||
> }
|
||
>
|
||
> fun main() { println(load(1)) } // данные 1
|
||
> ```
|
||
>
|
||
> **`finally` не влияет на результат `try`-выражения** - значение из него отбрасывается.
|
||
>
|
||
> **`return` в `finally` проглатывает исключение:**
|
||
> ```kotlin
|
||
> fun bad(): String {
|
||
> try {
|
||
> throw IllegalStateException("потеряется")
|
||
> } finally {
|
||
> return "из finally" // исключение исчезло!
|
||
> }
|
||
> }
|
||
>
|
||
> fun main() { println(bad()) } // из finally
|
||
> ```
|
||
> Исключение не дойдёт до вызывающего. `return` в `finally` писать нельзя.
|
||
>
|
||
> **Перехват `Exception` ловит все `RuntimeException`,** включая `NullPointerException` в вашем коде - то есть маскирует собственные баги под «обработанную ошибку».
|
||
>
|
||
> **`CancellationException` нельзя проглатывать в корутинах** - иначе отмена перестанет работать.
|
||
>
|
||
> **Исключение в `init` или в инициализаторе свойства оставляет побочные эффекты.** Объект не создастся, но то, что уже выполнилось, останется. Подробнее: [Инициализация](../ООП/Инициализация.md)
|
||
>
|
||
> **Создание исключения дорого** из-за сбора стектрейса. Для ожидаемых ситуаций в горячем коде используйте `null` или `Result`:
|
||
> ```kotlin
|
||
> fun main() {
|
||
> println("abc".toIntOrNull() ?: 0) // без исключения
|
||
> // println("abc".toInt()) // исключение + стектрейс
|
||
> }
|
||
> ```
|
||
>
|
||
> **Исключение из лямбды в `forEach` прерывает весь цикл**, а не одну итерацию:
|
||
> ```kotlin
|
||
> fun main() {
|
||
> val results = mutableListOf<Int>()
|
||
> try {
|
||
> listOf("1", "abc", "3").forEach { results.add(it.toInt()) }
|
||
> } catch (e: NumberFormatException) {
|
||
> println("прервано на второй строке, собрано: $results") // [1]
|
||
> }
|
||
> }
|
||
> ```
|
||
> Чтобы обработать все элементы, перехватывайте внутри лямбды или используйте `mapNotNull { it.toIntOrNull() }`.
|
||
>
|
||
> **`@Throws` не проверяется компилятором Kotlin** - это только метаданные для Java. Можно объявить исключение, которое никогда не бросается, и наоборот.
|
||
|
||
Далее: [Result и runCatching](Result%20и%20runCatching.md)
|