Files
obsidian/Kotlin/Исключения/Система исключений.md
T

371 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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)