Двойной против BigDecimal?
Я должен вычислить некоторые переменные с плавающей точкой, и мой коллега предлагает мне использовать BigDecimal
вместо double
так как будет точнее. Но я хочу знать, что это такое и как максимально использовать BigDecimal
?
10 ответов
BigDecimal
это точный способ представления чисел. Double
имеет определенную точность. Работа с двойниками различной величины (скажем, d1=1000.0
а также d2=0.001
) может привести к 0.001
отбрасывается при суммировании, так как разница в величине очень велика. С BigDecimal
этого бы не случилось.
Недостаток BigDecimal
является то, что это медленнее, и немного сложнее программировать алгоритмы таким образом (из-за +
-
*
а также /
не перегружен).
Если вы имеете дело с деньгами, или точность необходима, используйте BigDecimal
, Иначе Doubles
как правило, достаточно хорошо.
Я рекомендую прочитать Javadoc BigDecimal
как они объясняют вещи лучше, чем я здесь:)
Мой английский не очень хорош, поэтому я просто напишу простой пример здесь.
double a = 0.02;
double b = 0.03;
double c = b - a;
System.out.println(c);
BigDecimal _a = new BigDecimal("0.02");
BigDecimal _b = new BigDecimal("0.03");
BigDecimal _c = _b.subtract(_a);
System.out.println(_c);
Выход программы:
0.009999999999999998
0.01
Кто-нибудь еще хочет использовать double?;)
Есть два основных отличия от двойного:
- Произвольная точность, подобно BigInteger, они могут содержать число произвольной точности и размера.
- Base 10 вместо Base 2, BigDecimal - это масштаб n*10^, где n - произвольное большое целое число со знаком, а масштаб можно рассматривать как количество цифр для перемещения десятичной точки влево или вправо
Причина, по которой вы должны использовать BigDecimal для денежных расчетов, состоит не в том, что он может представлять любое число, а в том, что он может представлять все числа, которые могут быть представлены в десятичном представлении и которые включают практически все числа в денежном мире (вы никогда не переводите 1/3 $ кому-то).
Если вы хотите записать значение как 1/7 в качестве десятичного значения, вы получите
1/7 = 0.142857142857142857142857142857142857142857...
с бесконечной последовательностью 142857. Но так как вы можете записать только конечное число цифр, вы неизбежно внесете ошибку округления (или усечения).
К сожалению, числа типа 1/10 или 1/100, выраженные в виде двоичных чисел с дробной частью, также имеют бесконечное число двоичных чисел в десятичных числах.
1/10 = binary 0.000110011001100110...
Двойные значения хранятся в виде двоичных чисел и, следовательно, могут привести к ошибке только путем преобразования десятичного числа в двоичное число, даже не выполняя никакой арифметики.
Десятичные числа (как BigDecimal
), с другой стороны, сохраняйте каждую десятичную цифру как есть. Это означает, что десятичный тип не является более точным, чем двоичный тип с плавающей запятой или тип с фиксированной запятой в общем смысле (например, он не может хранить 1/7 без потери точности), но он более точен для чисел, заданных с конечным числом десятичные цифры, как это часто бывает при расчетах денег.
в Java BigDecimal
имеет дополнительное преимущество, заключающееся в том, что он может иметь произвольное (но конечное) количество цифр с обеих сторон десятичной точки, ограниченное только доступной памятью.
Если вы имеете дело с расчетами, существуют законы о том, как вы должны рассчитывать и какую точность вы должны использовать. Если вы потерпите неудачу, вы будете делать что-то незаконное. Единственная реальная причина в том, что битовое представление десятичных регистров не является точным. Как сказал Бэзил, лучшим объяснением является пример. Просто чтобы дополнить его пример, вот что происходит:
static void theDoubleProblem1() {
double d1 = 0.3;
double d2 = 0.2;
System.out.println("Double:\t 0,3 - 0,2 = " + (d1 - d2));
float f1 = 0.3f;
float f2 = 0.2f;
System.out.println("Float:\t 0,3 - 0,2 = " + (f1 - f2));
BigDecimal bd1 = new BigDecimal("0.3");
BigDecimal bd2 = new BigDecimal("0.2");
System.out.println("BigDec:\t 0,3 - 0,2 = " + (bd1.subtract(bd2)));
}
Выход:
Double: 0,3 - 0,2 = 0.09999999999999998
Float: 0,3 - 0,2 = 0.10000001
BigDec: 0,3 - 0,2 = 0.1
Также у нас есть что:
static void theDoubleProblem2() {
double d1 = 10;
double d2 = 3;
System.out.println("Double:\t 10 / 3 = " + (d1 / d2));
float f1 = 10f;
float f2 = 3f;
System.out.println("Float:\t 10 / 3 = " + (f1 / f2));
// Exception!
BigDecimal bd3 = new BigDecimal("10");
BigDecimal bd4 = new BigDecimal("3");
System.out.println("BigDec:\t 10 / 3 = " + (bd3.divide(bd4)));
}
Дает нам вывод:
Double: 10 / 3 = 3.3333333333333335
Float: 10 / 3 = 3.3333333
Exception in thread "main" java.lang.ArithmeticException: Non-terminating decimal expansion
Но:
static void theDoubleProblem2() {
BigDecimal bd3 = new BigDecimal("10");
BigDecimal bd4 = new BigDecimal("3");
System.out.println("BigDec:\t 10 / 3 = " + (bd3.divide(bd4, 4, BigDecimal.ROUND_HALF_UP)));
}
Имеет выход:
BigDec: 10 / 3 = 3.3333
BigDecimal - это числовая библиотека Oracle с произвольной точностью. BigDecimal является частью языка Java и полезен для самых разных приложений, от финансовых до научных (вот где я).
Нет ничего плохого в использовании двойников для определенных вычислений. Предположим, однако, что вы хотите вычислить Math.Pi * Math.Pi / 6, то есть значение дзета-функции Римана для реального аргумента два (проект, над которым я сейчас работаю). Деление с плавающей точкой ставит вас перед болезненной проблемой ошибки округления.
BigDecimal, с другой стороны, включает в себя множество опций для вычисления выражений с произвольной точностью. Методы добавления, умножения и деления, описанные в документации Oracle ниже, "заменяют" +, * и / в BigDecimal Java World:
http://docs.oracle.com/javase/7/docs/api/java/math/BigDecimal.html
Метод CompareTo особенно полезен в циклах while и for.
Однако будьте осторожны при использовании конструкторов для BigDecimal. Конструктор строк очень полезен во многих случаях. Например, код
BigDecimal onethird = new BigDecimal("0.33333333333");
использует строковое представление 1/3 для представления этого бесконечно повторяющегося числа с заданной степенью точности. Ошибка округления, скорее всего, где-то так глубоко внутри JVM, что ошибки округления не будут мешать большинству ваших практических расчетов. Однако, по личному опыту, я видел, как округление приближается. Метод setScale важен в этом отношении, как видно из документации Oracle.
Ваш коллега просто хочет упростить задачу.
Почти каждое значение x , записанное как десятичное дробное число, не может быть точно сохранено в двоичном формате — таким образом, вместо x будет храниться некоторое x + delta . Само по себе это не проблема, так как в большинстве случаев преобразование обратно в десятичное число обрабатывается правильно (например:Double.toString(0.1) -> "0.1"
). Но когда вы выполняете арифметические вычисления, в результате накапливаются дельты аргументов, и об этом нужно позаботиться.
Часто говорят, что невозможно накопить достаточно неточностей, чтобы повлиять на результат, посколькуdouble
имеет точность 1e-16 (т.е. |дельта| < 1e-16). Это верно для x около 1. Для x около 1M точность равна 1e-10; для 100B (вполне типичная сумма в индонезийских рупиях ) точность падает до 1e-5. Теперь у вас есть ограничение в 100 тысяч простых арифметических операций, чтобы гарантированно оставаться в безопасной зоне.
Даже если вы абсолютно уверены, что накопленная неточность не распространится на значительную часть результата, вам необходимо реализовать некоторые правила для ее подавления: округление, преобразование из центов и т. д. — каждый раз, когда результат следует рассматривать как десятичный, т.е. сравнивать, распечатать, сохранить в базе данных, отправить дальше и т. д. И ваш коллега должен все это просмотреть.
СBigDecimal
, где десятичные дроби хранятся точно, учитывая, что все денежные суммы в современном мире считаются десятичными, у вас с коллегой этой головной боли вообще нет.
Если вам нужно использовать деление в арифметике, вам нужно использовать double вместо BigDecimal. Разделение(divide(BigDecimal) method)
в BigDecimal довольно бесполезен, так как BigDecimal не может обрабатывать повторяющиеся десятичные рациональные числа (деление, в котором находятся делители, и будет выбрасыватьjava.lang.ArithmeticException: Non-terminating decimal expansion;
нет точного представимого десятичного результата.
Просто попробуйBigDecimal.ONE.divide(new BigDecimal("3"));
Double, с другой стороны, будет нормально обрабатывать деление (с понятной точностью, которая составляет примерно 15 значащих цифр)
package j2ee.java.math;
/**
* Generated from IDL definition of "valuetype "BigDecimal""
* TomORB IDL compiler v1.0
*/
public abstract class BigDecimal extends j2ee.java.lang.Number implements org.omg.CORBA.portable.StreamableValue, j2ee.java.lang.Comparable
{
private String[] _truncatable_ids = {"RMI:java.math.BigDecimal:11F6D308F5398BBD:54C71557F981284F"};
protected int scale_;
protected j2ee.java.math.BigInteger intVal;
/* constants */
int ROUND_UP = 0;
int ROUND_DOWN = 1;
int ROUND_CEILING = 2;
int ROUND_FLOOR = 3;
int ROUND_HALF_UP = 4;
int ROUND_HALF_DOWN = 5;
int ROUND_HALF_EVEN = 6;
int ROUND_UNNECESSARY = 7;
public abstract int _hashCode();
public abstract int scale();
public abstract int signum();
public abstract boolean _equals(org.omg.CORBA.Any arg0);
public abstract java.lang.String _toString();
public abstract j2ee.java.math.BigDecimal abs();
public abstract j2ee.java.math.BigDecimal negate();
public abstract j2ee.java.math.BigDecimal movePointLeft(int arg0);
public abstract j2ee.java.math.BigDecimal movePointRight(int arg0);
public abstract j2ee.java.math.BigDecimal setScale(int arg0);
public abstract j2ee.java.math.BigDecimal setScale(int arg0, int arg1);
public abstract j2ee.java.math.BigDecimal valueOf(long arg0);
public abstract j2ee.java.math.BigDecimal valueOf(long arg0, int arg1);
public abstract int compareTo(j2ee.java.math.BigDecimal arg0);
public abstract j2ee.java.math.BigInteger toBigInteger();
public abstract j2ee.java.math.BigInteger unscaledValue();
public abstract j2ee.javax.rmi.CORBA.ClassDesc classU0024(java.lang.String arg0);
public abstract j2ee.java.math.BigDecimal add(j2ee.java.math.BigDecimal arg0);
public abstract j2ee.java.math.BigDecimal max(j2ee.java.math.BigDecimal arg0);
public abstract j2ee.java.math.BigDecimal min(j2ee.java.math.BigDecimal arg0);
public abstract j2ee.java.math.BigDecimal multiply(j2ee.java.math.BigDecimal arg0);
public abstract j2ee.java.math.BigDecimal subtract(j2ee.java.math.BigDecimal arg0);
public abstract j2ee.java.math.BigDecimal divide(j2ee.java.math.BigDecimal arg0, int arg1);
public abstract j2ee.java.math.BigDecimal divide(j2ee.java.math.BigDecimal arg0, int arg1, int arg2);
public void _write (org.omg.CORBA.portable.OutputStream os)
{
super._write( os );
os.write_long(scale_);
((org.omg.CORBA_2_3.portable.OutputStream)os).write_value( new java.lang.String("intVal") );
}
public void _read (final org.omg.CORBA.portable.InputStream os)
{
super._read( os );
scale_=os.read_long();
intVal=(j2ee.java.math.BigInteger)((org.omg.CORBA_2_3.portable.InputStream)os).read_value ( "RMI:java.math.BigInteger:E2F79B6E7A470003:8CFC9F1FA93BFB1D".toString() );
}
public String[] _truncatable_ids()
{
return _truncatable_ids;
}
public org.omg.CORBA.TypeCode _type()
{
return j2ee.java.math.BigDecimalHelper.type();
}
}
Примитивные числовые типы полезны для хранения отдельных значений в памяти. Но при вычислениях с использованием типов double и float возникают проблемы с округлением. Это происходит из-за того, что представление в памяти не отображается точно в значение. Например, двойное значение должно занимать 64 бита, но Java не использует все 64 бита. Оно хранит только то, что считает важными частями числа. Таким образом, вы можете получить неправильные значения при добавлении значений типа float или double. Может быть, это видео https://youtu.be/EXxUSz9x7BM объяснит больше