обзор
В предыдущей статье я описал, почему BigDecimal большую часть времени не является ответом. Хотя можно создавать ситуации, в которых double вызывает ошибку, также легко создавать ситуации, когда BigDecimal получает ошибку.
BigDecimal легче понять, но легче ошибиться.
Неподтвержденное свидетельство — то, что у младших разработчиков нет такой большой проблемы с правильным BigDecimal, как они получают двойной с правильным округлением. Тем не менее, я скептически отношусь к этому, потому что в BigDecimal ошибка также намного легче остается незамеченной.
Давайте возьмем этот пример, где double дает неправильный ответ.
1
2
3
4
5
6
7
8
9
|
double d = 1.00 ; d /= 49 ; d *= 49 * 2 ; System.out.println( "d=" + d); BigDecimal bd = BigDecimal.ONE; bd = bd .divide(BigDecimal.valueOf( 49 ), 2 , BigDecimal.ROUND_HALF_UP); bd = bd.multiply(BigDecimal.valueOf( 49 * 2 )); System.out.println( "bd=" + bd); |
печать
1
2
|
d= 1.9999999999999998 bd= 1.96 |
В этом случае double выглядит неправильно, ему нужно округление, которое даст правильный ответ 2.0. Однако BigDecimal выглядит правильно, но это не из-за ошибки представления. Мы могли бы изменить деление для большей точности, но вы всегда получите ошибку представления, хотя вы можете контролировать, насколько мала эта ошибка.
Вы должны убедиться, что числа реальны и использовать округление.
Даже с BigDecimal вы должны использовать соответствующее округление. Допустим, у вас есть кредит на 1 000 000 долларов и вы применяете 0,0005% годовых в день. Учетная запись может иметь только целое количество центов, поэтому округление необходимо, чтобы получить реальную сумму денег. Если не делать этого, сколько времени потребуется, чтобы сделать разницу в 1 цент?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
double interest = 0.0005 ; BigDecimal interestBD = BigDecimal.valueOf(interest); double amount = 1e6; BigDecimal amountBD = BigDecimal.valueOf(amount); BigDecimal amountBD2 = BigDecimal.valueOf(amount); long i = 0 ; do { System.out.printf( "%,d: BigDecimal: $%s, BigDecimal: $%s%n" , i, amountBD, amountBD2); i++; amountBD = amountBD.add(amountBD.multiply(interestBD) .setScale( 2 , BigDecimal.ROUND_HALF_UP)); amountBD2 = amountBD2.add(amountBD2.multiply(interestBD)); } while (amountBD2.subtract(amountBD).abs() .compareTo(BigDecimal.valueOf( 0.01 )) < 0 ); System.out.printf( "After %,d iterations the error was 1 cent and you owe %s%n" , i, amountBD); |
наконец печатает
1
2
3
|
8 : BigDecimal: $ 1004007.00 , BigDecimal: $ 1004007.00700437675043756250390625000000000000000 After 9 iterations the error was 1 cent and you owe 1004509.00 |
Вы можете округлить результат, но это скрывает тот факт, что вы потеряли цент, даже если вы использовали BigDecimal.
двойной в конечном итоге имеет ошибку представления
Даже если вы используете соответствующее округление, double даст вам неверный результат. Это намного позже, чем в предыдущем примере.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
double interest = 0.0005 ; BigDecimal interestBD = BigDecimal.valueOf(interest); double amount = 1e6; BigDecimal amountBD = BigDecimal.valueOf(amount); long i = 0 ; do { System.out.printf( "%,d: double: $%.2f, BigDecimal: $%s%n" , i, amount, amountBD); i++; amount = round2(amount + amount * interest); amountBD = amountBD.add(amountBD.multiply(interestBD) .setScale( 2 , BigDecimal.ROUND_HALF_UP)); } while (BigDecimal.valueOf(amount).subtract(amountBD).abs() .compareTo(BigDecimal.valueOf( 0.01 )) < 0 ); System.out.printf( "After %,d iterations the error was 1 cent and you owe %s%n" , i, amountBD); |
наконец печатает
1
2
|
22 , 473 : double : $ 75636308370.01 , BigDecimal: $ 75636308370.01 After 22 , 474 iterations the error was 1 cent and you owe 75674126524.20 |
С точки зрения ИТ мы имеем ошибку в один цент, с точки зрения бизнеса у нас есть клиент, который не выплачивал более 9 лет и должен банку 75,6 миллиарда долларов, что достаточно, чтобы обанкротить банк. Если бы только ИТ-специалист использовал BigDecimal !?
Вывод
Моя последняя рекомендация: вам следует использовать то, что вам удобно, не забывать о округлении, использовать реальные числа, а не то, что производит математика, например, могу ли я получить доли цента или я могу торговать долями доли. Не забывайте о перспективах бизнеса. Вы можете обнаружить, что BigDecimal имеет больше смысла для вашей компании, вашего проекта или вашей команды.
Не думайте, что BigDecimal — единственный способ, не думайте, что проблемы с двойными гранями не применимы и к BigDecimal. BigDecimal не является билетом для лучшей практики кодирования, потому что самодовольство — верный способ введения ошибок.
Ссылка: | Сложная двойная ошибка от нашего партнера JCG Питера Лоури из блога Vanilla Java . |