232 lines
11 KiB
Markdown
232 lines
11 KiB
Markdown
---
|
||
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)
|