Сбой подмножества java BigDecimal

Я попробовал следующий код. но получая другой результат при вычитании с использованием BigDecimal.

    double d1 = 0.1;
    double d2 = 0.1;
    System.out.println("double result: "+ (d2-d1));

    float f1 = 0.1F;
    float f2 = 0.1F;
    System.out.println("float result: "+ (f2-f1));

    BigDecimal b1 = new BigDecimal(0.01);
    BigDecimal b2 = new BigDecimal(0.01);

    b1 = b1.subtract(b2);
    System.out.println("BigDecimal result: "+ b1);

Результат:

double result: 0.0
float result: 0.0
BigDecimal result: 0E-59

Я все еще работаю над этим. может кто-нибудь уточнить.


person user1514499    schedule 15.03.2013    source источник
comment
0E-59 по-прежнему равен нулю, а шкала получается из шкалы входных данных.   -  person user207421    schedule 16.03.2013
comment
Ответы объясняют причину, и вот простой совет: если вы используете BigDecimal, вам нужна полезная шкала, поэтому, когда вы вызываете c-tor с некоторым двойным значением, всегда включайте MathContext.   -  person bestsss    schedule 17.03.2013


Ответы (8)


Интересно, что значения кажутся равными, и вычитание дает вам ноль, похоже, это просто проблема с кодом печати. Следующий код:

import java.math.BigDecimal;
public class Test {
    public static void main(String args[]) {
        BigDecimal b1 = new BigDecimal(0.01);
        BigDecimal b2 = new BigDecimal(0.01);
        BigDecimal b3 = new BigDecimal(0);
        if (b1.compareTo(b2) == 0) System.out.println("equal 1");
        b1 = b1.subtract(b2);
        if (b1.compareTo(b3) == 0) System.out.println("equal 2");
        System.out.println("BigDecimal result: "+ b1);
    }                          
}

выводит оба сообщения equal, указывая, что значения одинаковы одинаковы и что при вычитании вы получаете ноль.

Вы можете попытаться поднять это как ошибку и посмотреть, что вернется Oracle. Скорее всего, они просто заявят, что 0e-59 по-прежнему равно нулю, так что это не ошибка, или что довольно сложное поведение, описанное в страница документации BigDecimal работает должным образом. В частности, пункт, который гласит:

Существует однозначное соответствие между различимыми значениями BigDecimal и результатом этого преобразования. То есть каждое различимое значение BigDecimal (немасштабированное значение и масштаб) имеет уникальное строковое представление в результате использования toString. Если это строковое представление преобразуется обратно в BigDecimal с помощью конструктора BigDecimal(String), исходное значение будет восстановлено.

Тот факт, что исходное значение должно быть восстанавливаемым, означает, что toString() необходимо генерировать уникальную строку для каждой шкалы, поэтому вы получаете 0e-59. В противном случае преобразование строки back в BigDecimal может дать вам другое значение (кортеж немасштабированного значения/масштаба).

Если вы действительно хотите, чтобы ноль отображался как "0" независимо от масштаба, вы можете использовать что-то вроде:

if (b1.compareTo(BigDecimal.ZERO) == 0) b1 = new BigDecimal(0);
person paxdiablo    schedule 15.03.2013
comment
Я не думаю, что это ошибка; BigDecimal.toString печатает каноническое представление, поэтому оно должно однозначно представлять как коэффициент масштабирования, так и немасштабированное значение. Масштабный коэффициент равен 59, потому что масштабный коэффициент входных данных для вычитания был 59... - person Oliver Charlesworth; 15.03.2013
comment
@ Оли, на самом деле ты прав, такое поведение преднамеренно в соответствии с документом. - person paxdiablo; 15.03.2013

[Здесь есть много ответов, говорящих вам, что двоичные числа с плавающей запятой не могут точно представлять 0,01, и подразумевающих, что результат, который вы видите, является каким-то неточным. Хотя первая часть этого утверждения верна, на самом деле это не основная проблема.]


Ответ заключается в том, что "0E-59" равно равно 0. Напомним, что BigDecimal представляет собой комбинацию немасштабированного значения и десятичного коэффициента масштабирования:

System.out.println(b1.unscaledValue());
System.out.println(b1.scale());

отображает:

0
59

Немасштабированное значение равно 0, как и ожидалось. «Странное» значение масштаба — это просто артефакт десятичного расширения неточного представления с плавающей запятой 0,01:

System.out.println(b2.unscaledValue());
System.out.println(b2.scale());

отображает:

1000000000000000020816681711721685132943093776702880859375
59

Следующий очевидный вопрос: почему BigDecimal.toString просто не отображает b1 как "0" для удобства? Ответ заключается в том, что строковое представление должно быть однозначным. Из Javadoc для toString :

Существует однозначное соответствие между различимыми значениями BigDecimal и результатом этого преобразования. То есть каждое различимое значение BigDecimal (немасштабированное значение и масштаб) имеет уникальное строковое представление в результате использования toString. Если это строковое представление преобразуется обратно в BigDecimal с помощью конструктора BigDecimal(String), исходное значение будет восстановлено.

Если бы он просто отображал «0», вы не смогли бы вернуться к этому точному объекту BigDecimal.

person Oliver Charlesworth    schedule 15.03.2013
comment
Я пробовал с BigDecimal b1 = new BigDecimal(0.05); BigDecimal b2 = новый BigDecimal (0,01); получение результата BigDecimal 1: 4000000000000000256739074444567449972964823246002197265625 результата BigDecimal 2: 59. похоже на ваш... - person user1514499; 15.03.2013
comment
@ user1514499: Хорошо, это другой вопрос! Но ответ связан; вы не можете точно представить 0.01 или 0.05 в двоичном формате с плавающей запятой. Они приблизительны. - person Oliver Charlesworth; 15.03.2013
comment
Черт, когда я редактировал этот последний фрагмент (материал двунаправленного преобразования) в свой ответ, я вышел на воздух и обнаружил, что вы уже прибили его :-) - person paxdiablo; 15.03.2013
comment
+1 Я узнал кое-что новое. Хорошо объяснил! (Удалил мой ответ, так как он не имеет значения) - person Eyal Schneider; 15.03.2013

Использовать конструктор из строки: b1 = new BigDecimal("0.01");

Потеря точности Java

(слайд 23) http://strangeoop2010.com/system/talks/presentations/000/014/450/BlochLee-JavaPuzzlers.pdf

person Ivan Borisov    schedule 15.03.2013
comment
Конечно. Но это не отвечает на вопрос; почему разница напечатана странно? - person Oliver Charlesworth; 15.03.2013
comment
Вы говорите, что для b1 и b2 будут установлены разные значения, несмотря на то, что они созданы одинаково? - person paxdiablo; 15.03.2013
comment
Так работает BigDecimal. Результатом вычитания является BigDecimal, в котором хранится ровно 0, но он унаследовал масштаб (e-59) от плохого BigDecimal, построенного из 0,01. - person Ivan Borisov; 15.03.2013
comment
@IvanBorisov: Хорошо, тогда это (0e-59 равно 0) является реальным ответом, вы должны сказать это явно! - person Oliver Charlesworth; 15.03.2013

Вы должны получить возвращаемое значение:

BigDecimal b3 = b1.subtract(b2);
System.out.println("BigDecimal result: "+ b3);
person ghdalum    schedule 15.03.2013
comment
Он делает это b1 = b1.subtract(b2); - person Austin; 15.03.2013
comment
Без проблем! После редактирования вопроса у вас возникла проблема с ограниченной точностью чисел с плавающей запятой, которую этот ответ, очевидно, не исправляет :) - person ghdalum; 15.03.2013

BigDecimal(двойное значение)

1.Результаты этого конструктора могут быть несколько непредсказуемыми. Можно предположить, что запись new BigDecimal(0.1) в Java создает BigDecimal, который точно равен 0,1 (немасштабированное значение 1 с масштабом 1), но на самом деле он равен 0,10000000000000000055511151231257827021181583404541015625. Это связано с тем, что 0,1 нельзя представить точно как двойное число (или, если на то пошло, как двоичную дробь любой конечной длины). Таким образом, значение, которое передается конструктору, не равно точно 0,1, несмотря на внешний вид.

2. Конструктор String, с другой стороны, совершенно предсказуем: запись new BigDecimal("0.1") создает BigDecimal, который точно равен 0,1, как и следовало ожидать. Поэтому обычно рекомендуется использовать конструктор String вместо этого.

3. Когда в качестве источника для BigDecimal необходимо использовать двойное число, обратите внимание, что этот конструктор обеспечивает точное преобразование; это не дает того же результата, что и преобразование double в String с использованием метода Double.toString(double) и последующего использования конструктора BigDecimal(String). Чтобы получить этот результат, используйте статический метод valueOf(double).

person Achintya Jha    schedule 15.03.2013
comment
Конечно. Но это не отвечает на вопрос; почему разница напечатана странно? - person Oliver Charlesworth; 15.03.2013

Итак, настоящий вопрос: со следующим кодом

BigDecimal b1 = new BigDecimal(0.01);
BigDecimal b2 = new BigDecimal(0.01);
b1 = b1.subtract(b2);

почему b1.toString() оценивается как "0E-59", а не как "0.0", "0E0" или просто "0"?


Причина в том, что toString() печатает канонический формат BigDecimal. См. BigDecimal.toString() для получения дополнительной информации.

В конце концов, 0E-59 равно 0.0 — это 0*10^59, которое математически оценивается как 0. Итак, неожиданный результат — это вопрос внутреннего представления BigDecimal.

Чтобы получить значения float или double, используйте

b1.floatValue());

or

b1.doubleValue());

Оба оцениваются как 0.0.

person Andreas Fester    schedule 15.03.2013
comment
Но опять же, я бы ожидал, что b1 и b2 будут точно такими же, ни один из них не равен 0,01. Таким образом, вычитание должно дать ровно 0. - person paxdiablo; 15.03.2013

Это известная проблема, BigDecimal(double val) API. Результаты этого конструктора могут быть несколько непредсказуемыми. Хотя в этой интерпретации это выглядит очень странно. Настоящая причина в том, что новый BigDecimal(0.01) создает BigDecimal с приблизительными значениями.

0.01000000000000000020816681711721685132943093776702880859375

который имеет длинную точность, поэтому результат вычитания также имеет длинную точность.

В любом случае, мы можем решить "проблему" таким образом

BigDecimal b1 = new BigDecimal("0.01");
BigDecimal b2 = new BigDecimal("0.01");

или мы можем использовать конструктор с установкой точности

BigDecimal b1 = new BigDecimal(0.01, new MathContext(1));
BigDecimal b2 = new BigDecimal(0.01, new MathContext(1));
person Evgeniy Dorofeev    schedule 15.03.2013
comment
BigDecimal(double val) API Результаты этого конструктора могут быть несколько непредсказуемыми. - person Evgeniy Dorofeev; 15.03.2013
comment
Здесь нужно согласиться с Оли. BigDec(0.1) не даст вам 0.1, но он должен дать вам то же самое, если вы сделаете это дважды. - person paxdiablo; 15.03.2013

Используйте так:

BigDecimal b1 = BigDecimal.valueOf(0.01);
BigDecimal b2 = BigDecimal.valueOf(0.01);

b1 = b1.subtract(b2);
System.out.println("BigDecimal result: "+ b1);
person user7046332    schedule 20.10.2016