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

11 KiB
Raw Blame History

title, description, tags
title description tags
Числа и переполнение Переполнение целых, точность Double, BigDecimal для денег и математические функции
kotlin
stdlib

Переполнение целых

Kotlin, как и Java, не проверяет переполнение. При выходе за границы значение «заворачивается»:

println(Int.MAX_VALUE)        // 2147483647
println(Int.MAX_VALUE + 1)    // -2147483648 — тихо
println(Int.MIN_VALUE - 1)    // 2147483647

Это не исключение и даже не предупреждение - просто неверный результат.

Границы типов:

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, результат которого не влезает:

val ms = 1000 * 60 * 60 * 24 * 30         // -1702967296 — переполнение!
val msOk = 1000L * 60 * 60 * 24 * 30      // 2592000000 — первый операнд Long
println("$ms $msOk")

Правило: если промежуточный результат может превысить 2 млрд, делайте Long первый операнд. Приписать L к последнему множителю недостаточно - умножение идёт слева направо.

Проверяемая арифметика

Явные функции, бросающие исключение при переполнении:

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 аналогов нет.

Деление

println(7 / 2)        // 3   — целочисленное, дробная часть отброшена
println(-7 / 2)       // -3  — округление к нулю, не к меньшему
println(7 % 2)        // 1
println(-7 % 2)       // -1  — знак от делимого
println(7.0 / 2)      // 3.5

Деление на целочисленный ноль - исключение, на вещественный - нет:

// println(1 / 0)     // ArithmeticException: / by zero
println(1.0 / 0)      // Infinity
println(-1.0 / 0)     // -Infinity
println(0.0 / 0)      // NaN

Остаток для отрицательных

% в Kotlin даёт остаток со знаком делимого, что ломает индексацию по кругу:

println(-1 % 3)                  // -1 — не 2!
println((-1).mod(3))             // 2  — математический модуль
println((-1).rem(3))             // -1 — то же, что %

Для циклических индексов нужен mod, а не %.

Вещественные числа

Float и Double следуют IEEE 754 и не представляют десятичные дроби точно:

println(0.1 + 0.2)                 // 0.30000000000000004
println(0.1 + 0.2 == 0.3)          // false
println(1.0 - 0.9)                 // 0.09999999999999998

Сравнивать нужно с допуском:

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 и бесконечности

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 ведёт себя по-разному в операторах и в коллекциях - это редкая, но болезненная ловушка:

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 использовать нельзя: ошибки округления накапливаются.

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:

import java.math.BigDecimal

fun main() {
    println(BigDecimal(0.1))        // 0.1000000000000000055511151231257827...
    println(BigDecimal("0.1"))      // 0.1
}

BigDecimal.equals сравнивает и масштаб, поэтому 2.0 и 2.00 не равны:

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, не ==.

Математические функции

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.

Ограничение диапазона

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

Преобразования и потеря данных

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

Представление и биты

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

[!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.

Дополнительно: Типы данных Далее: Массивы