11 KiB
title, description, tags
| title | description | tags | ||
|---|---|---|---|---|
| Числа и переполнение | Переполнение целых, точность Double, BigDecimal для денег и математические функции |
|
Переполнение целых
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_VALUEInt.MAX_VALUEInteger.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.
Дополнительно: Типы данных Далее: Массивы