Отличия типа BigDecimal от double/float, что и где лучше использовать?
Я прочитал статью про BigDecimal, и у меня вопрос почему double/float являются не точными типами? Почему не сделали double/float точными, что бы мы не использовали лишний тип BigDecimal, а все работало бы double/float?
Ответы (3 шт):
- double/float - не точно, но очень быстро.
- BigDecimal - точно, но не быстро.
Плюс исторические причины.
Количество чисел с плавающей точке континуум, то есть их бесконечно много - причем в любом в сколь угодно малом отрезке (теорема Кантора - если мне память не изменяет), а в реализации Float/Double их по определению конечное количество, отсюда и выходит, что даже например примитивный sqrt(2) всегда будет аппроксимироваться некой точностью (для double 15 знаков после запятой). Отсюда и вытекает, что вычисления с плавающей точкой всегда приблизительны и не точны.
Есть альтернатива в виде BigDecimal позволяет вести вычисления с плавающей точкой любой точности, правда медленно.
double и float - являются нативными для процессора типами. Т.е. процессор имеет команды заточенные под данные типы. Размер этих типов 64 и 32 бита соответственно. Т.е. имеются некие предельные значения, которые могут храниться в этих типах. BigDecimal же является более сложной структурой размером гораздо больше 64-х бит, которой процессор напрямую оперировать не может. Однако BigDecimal может хранить более точные значения.
Чтобы провести аналогию лучше рассмотреть отношение int/long к BigInteger. Первые два типа имеют размер 32 и 64 бита. Второй же тип мы условно можем считать безразмерным. Максимальное значение для java-типа long (2^63-1). Если 2^10=1024 ~10^3, т.е. примерно 1_000, то 2^63 ~ 2^3 * 10^18 ~ 8*10^18
Теперь посчитаем сумму:
1_000_000_000_000_000_000 //18 нулей и меньше 8*10^18
+
1_000_000_000_000_000_000 //18 нулей и меньше 8*10^18
=
2_000_000_000_000_000_000 //18 нулей и меньше 8*10^18
Как видно, результат вполне влазит в диапазон java-типа long. Более того, т.к. процессор заточен под сложение чисел такого диапазона, то можно назвать (условно) данную операцию элементарной, т.е. выполняющийся за 1 такт.
А теперь посчитаем сумму:
16_000_000_000_000_000_000 //18 нулей и больше 8*10^18
+
16_000_000_000_000_000_000 //18 нулей и больше 8*10^18
=
32_000_000_000_000_000_000 //18 нулей и больше 8*10^18
Эту операцию на процессоре уже так просто не выполнить. Даже любое из чисел не так просто будет записать. Придётся писать специальную программу, которая будет делать нечто подобное:
// 1 шаг - подгрузка младших частей и их сложение
6_000_000_000_000_000_000 //младшая часть
+
6_000_000_000_000_000_000 //младшая часть
=
2_000_000_000_000_000_000 //не 12, а 2, и есть флаг переполнения
// 2 шаг - подгрузка старших частей и их сложение
1 //старшая часть
+
1 //старшая часть
=
2
// 3 шаг - коррекция старшей части
2 //результат из шага 2
+
1 //1 - т.к. был флаг переполнения из шага 1
=
3
// 4 шаг
Запись 2_000_000_000_000_000_000 в память // результат из младшей части
Запись 3 в память // результат из старшей части
Как видно, для, казалось бы, простейшего сложения пришлось писать уже целую функцию. И эта функция уже не будет работать за 1 такт. Java как раз из коробки предоставляет набор таких функций через класс BigInteger. С отношением float/double к BigDecimal такая же ситуация. Всё это конечно упрощение, но общую логики доносит.
Вторая проблема - потеря точности. Из-за особенностей представления double/float при сложении очень больших (по модулю) чисел с очень маленькими результат может оказаться равен очень большому. Т.е. меньшее слагаемое вообще может быть проигнорировано. С BigDecimal такая проблема так же присутствует, но в меньшей степени.
Если же говорить коротко, то по возможности используй более простые типы, т.к. операции над ними быстрее. Типы BigInteger и BigDecimal стоит использовать, если очень большие числа и/или точность чрезвычайно критична.