Files
obsidian/Kotlin/Синтаксис и типы/Работа с данными/Числа и переполнение.md
T

232 lines
11 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: Переполнение целых, точность Double, BigDecimal для денег и математические функции
tags:
- kotlin
- stdlib
---
## Переполнение целых
Kotlin, как и Java, **не проверяет переполнение**. При выходе за границы значение «заворачивается»:
```kotlin
println(Int.MAX_VALUE) // 2147483647
println(Int.MAX_VALUE + 1) // -2147483648 — тихо
println(Int.MIN_VALUE - 1) // 2147483647
```
Это не исключение и даже не предупреждение - просто неверный результат.
Границы типов:
```kotlin
println("${Byte.MIN_VALUE}..${Byte.MAX_VALUE}") // -128..127
println("${Int.MIN_VALUE}..${Int.MAX_VALUE}") // -2147483648..2147483647
println("${Long.MIN_VALUE}..${Long.MAX_VALUE}") // -9223372036854775808..9223372036854775807
```
### Где это кусает
Самый частый случай - умножение `Int`, результат которого не влезает:
```kotlin
val ms = 1000 * 60 * 60 * 24 * 30 // -1702967296 — переполнение!
val msOk = 1000L * 60 * 60 * 24 * 30 // 2592000000 — первый операнд Long
println("$ms $msOk")
```
Правило: если промежуточный результат может превысить 2 млрд, делайте `Long` **первый** операнд. Приписать `L` к последнему множителю недостаточно - умножение идёт слева направо.
### Проверяемая арифметика
Явные функции, бросающие исключение при переполнении:
```kotlin
println(Math.addExact(1, 2)) // 3
// println(Math.addExact(Int.MAX_VALUE, 1)) // ArithmeticException
// println(Math.multiplyExact(100000, 100000)) // ArithmeticException
```
Доступны `addExact`, `subtractExact`, `multiplyExact`, `incrementExact`, `toIntExact`. Это функции JDK, в stdlib Kotlin аналогов нет.
## Деление
```kotlin
println(7 / 2) // 3 — целочисленное, дробная часть отброшена
println(-7 / 2) // -3 — округление к нулю, не к меньшему
println(7 % 2) // 1
println(-7 % 2) // -1 — знак от делимого
println(7.0 / 2) // 3.5
```
Деление на целочисленный ноль - исключение, на вещественный - нет:
```kotlin
// println(1 / 0) // ArithmeticException: / by zero
println(1.0 / 0) // Infinity
println(-1.0 / 0) // -Infinity
println(0.0 / 0) // NaN
```
### Остаток для отрицательных
`%` в Kotlin даёт остаток со знаком делимого, что ломает индексацию по кругу:
```kotlin
println(-1 % 3) // -1 — не 2!
println((-1).mod(3)) // 2 — математический модуль
println((-1).rem(3)) // -1 — то же, что %
```
Для циклических индексов нужен `mod`, а не `%`.
## Вещественные числа
`Float` и `Double` следуют IEEE 754 и **не представляют десятичные дроби точно**:
```kotlin
println(0.1 + 0.2) // 0.30000000000000004
println(0.1 + 0.2 == 0.3) // false
println(1.0 - 0.9) // 0.09999999999999998
```
Сравнивать нужно с допуском:
```kotlin
fun almostEqual(a: Double, b: Double, eps: Double = 1e-9) =
kotlin.math.abs(a - b) < eps
println(almostEqual(0.1 + 0.2, 0.3)) // true
```
### NaN и бесконечности
```kotlin
val nan = Double.NaN
println(nan == nan) // false! NaN не равен ничему, включая себя
println(nan.isNaN()) // true — единственный верный способ проверки
println(Double.POSITIVE_INFINITY) // Infinity
println(1.0.isFinite()) // true
println(0.0 == -0.0) // true
```
`NaN` ведёт себя по-разному в операторах и в коллекциях - это редкая, но болезненная ловушка:
```kotlin
println(Double.NaN == Double.NaN) // false — оператор
println(Double.NaN.equals(Double.NaN)) // true — метод equals
println(listOf(Double.NaN).contains(Double.NaN)) // true — через equals
```
## Деньги - BigDecimal
Для денег `Double` использовать нельзя: ошибки округления накапливаются.
```kotlin
import java.math.BigDecimal
import java.math.RoundingMode
fun main() {
val a = BigDecimal("0.1") // только из строки!
val b = BigDecimal("0.2")
println(a + b) // 0.3 — точно
println(a.add(b) == BigDecimal("0.3")) // true
val price = BigDecimal("100.555")
println(price.setScale(2, RoundingMode.HALF_UP)) // 100.56
// операторы перегружены, работают привычно
println(BigDecimal("10") * BigDecimal("3")) // 30
println(BigDecimal("10").divide(BigDecimal("4"))) // 2.5
}
```
Создавать **только из строки**: `BigDecimal(0.1)` подхватит уже испорченное значение `Double`:
```kotlin
import java.math.BigDecimal
fun main() {
println(BigDecimal(0.1)) // 0.1000000000000000055511151231257827...
println(BigDecimal("0.1")) // 0.1
}
```
`BigDecimal.equals` сравнивает и масштаб, поэтому `2.0` и `2.00` не равны:
```kotlin
import java.math.BigDecimal
fun main() {
println(BigDecimal("2.0") == BigDecimal("2.00")) // false
println(BigDecimal("2.0").compareTo(BigDecimal("2.00")) == 0) // true
}
```
Для сравнения значений - `compareTo`, не `==`.
## Математические функции
```kotlin
import kotlin.math.*
fun main() {
println(abs(-5)) // 5
println(max(3, 7)) // 7
println(min(3, 7)) // 3
println(sqrt(16.0)) // 4.0
println(2.0.pow(10)) // 1024.0
println(PI) // 3.141592653589793
println(round(2.5)) // 2.0 — не 3.0! округление к чётному
println(round(3.5)) // 4.0
println(floor(2.9)) // 2.0
println(ceil(2.1)) // 3.0
println(truncate(2.9)) // 2.0
println(2.567.roundToInt()) // 3
println(hypot(3.0, 4.0)) // 5.0
}
```
Пакет `kotlin.math` нужно импортировать - автоматически он не подключается.
Обратите внимание на `round`: при ровной половине он округляет **к ближайшему чётному** (IEEE 754 round-half-even), поэтому `round(2.5)` даёт `2.0`, а `round(3.5)` - `4.0`. Если нужно привычное «половина вверх», берите `roundToInt()`: `2.5.roundToInt()` даёт `3`.
### Ограничение диапазона
```kotlin
println(15.coerceIn(0, 10)) // 10
println((-5).coerceIn(0, 10)) // 0
println(5.coerceAtLeast(0)) // 5
println(15.coerceAtMost(10)) // 10
```
Удобнее, чем `max(0, min(10, x))`.
## Преобразования и потеря данных
```kotlin
import kotlin.math.roundToInt
fun main() {
println(300.toByte()) // 44 — старшие биты отброшены
println(3.99.toInt()) // 3 — отбрасывание, не округление
println((-3.99).toInt()) // -3
println(3.99.roundToInt()) // 4 — настоящее округление
println(Double.NaN.toInt()) // 0 — тихо!
println(1e20.toInt()) // 2147483647 — обрезано до MAX_VALUE
}
```
`toInt()` на `Double` никогда не падает - он тихо даёт неверный результат. Для контроля используйте `roundToInt()` и проверку `isFinite()`.
## Представление и биты
```kotlin
println(255.toString(2)) // 11111111 — в двоичной
println(255.toString(16)) // ff
println("ff".toInt(16)) // 255
println(5.countOneBits()) // 2 — число единичных битов
println(Integer.toBinaryString(-1)) // 32 единицы
```
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | `Math.abs(x)` | `kotlin.math.abs(x)` |
> | `Math.pow(2, 10)` | `2.0.pow(10)` |
> | `Integer.MAX_VALUE` | `Int.MAX_VALUE` |
> | `Integer.parseInt(s)` | `s.toInt()` |
> | `Integer.toBinaryString(n)` | `n.toString(2)` |
> | `Math.floorMod(-1, 3)` | `(-1).mod(3)` |
> | неявное `int` → `long` | только явный `toLong()` |
> | `Math.addExact` | то же (из JDK) |
>
> Поведение переполнения, деления и IEEE 754 полностью унаследовано от JVM - здесь Kotlin ничего не меняет и не защищает. Выигрыш только в читаемости: `2.0.pow(10)` вместо `Math.pow(2, 10)`, `coerceIn` вместо вложенных `min`/`max`.
>
> Запрет неявных преобразований - реальная защита: в Java `long total = intA * intB;` переполняется до присваивания, и компилятор молчит. В Kotlin такой код не соберётся без явного `toLong()`.
>
> Сравнить: [Типы данных в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Базовый%20синтаксис/Типы%20данных.md)
> [!warning] Подводные камни
> **`1000 * 60 * 60 * 24 * 30` переполняется** - самая частая реальная ошибка с числами. Все операнды `Int`, результат считается в `Int`, и только потом присваивается в `Long`.
>
> **`Double` для денег даёт расхождения в копейках**, которые накапливаются на больших объёмах. Только `BigDecimal` из строки или целые копейки в `Long`.
>
> **`NaN == NaN` ложно, а `listOf(NaN).contains(NaN)` истинно.** Сортировка и `distinct()` с `NaN` ведут себя неинтуитивно.
>
> **`toInt()` на `Double` обрезает, а `NaN` превращает в 0** - без единого предупреждения.
>
> **`%` для отрицательных чисел даёт отрицательный остаток.** Код `arr[(i - 1) % n]` упадёт на `i = 0`; нужен `arr[(i - 1).mod(n)]`.
>
> **`BigDecimal("2.0") != BigDecimal("2.00")`** - `equals` учитывает масштаб. Сравнивайте через `compareTo`.
Дополнительно: [Типы данных](../Базовый%20синтаксис/Типы%20данных.md)
Далее: [Массивы](Массивы.md)