--- 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)