Статьи

Сложная двойная ошибка

обзор

В предыдущей статье я описал, почему 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 .