Почему этот странный вывод с усечением и BigDecimal?

Я вызываю метод truncate для усечения двойного значения, так что после десятичной (без округления) должна быть одна цифра),
Для примера. truncate(123.48574) = 123.4.

Мой метод усечения - это что-то вроде этого

public double truncate (double x) {
    long y = (long) (x * 10);
    double z = (double) (y / 10);
    return z;
}

Он работает отлично для почти всех значений, кроме этого странного вывода.

double d = 0.787456;
d = truncate(d + 0.1); //gives 0.8 as expected. Okay.

Но,

double d = 0.7;
d = truncate(d + 0.1); //should also give 0.8. But its giving 0.7 only. 
                       //Strange I don't know why?

Infact работает отлично для всех остальных 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, -, 0.8, 0.9
Я имею в виду, например,

double d = 0.8;
d = truncate(d + 0.1); //gives 0.9 as expected

Я попробовал это с помощью BigDecimal. Но то же самое. Без изменений. Вот код для этого.

double d = 0.7;
BigDecimal a = new BigDecimal(d + 0.1);
BigDecimal floored = a.setScale(1, BigDecimal.ROUND_DOWN);
double d1 = floored.doubleValue();
System.out.println(d1); //Still gives 0.7

И снова реальный факт заключается в том, что он отлично работает с Math.round.

public double roundUp1d (double d) {
    return Math.round(d * 10.0) / 10.0;
}

Поэтому, если я назову roundUp1d(0.7 + 0.1), он даст 0.8, как ожидалось. Но я не хочу, чтобы значения округлялись, поэтому я не могу использовать это.

В чем проблема с 0.7?

Ответ 1

Плавающие точки по своей сути неточны. Другие ответы здесь уже объясняют теорию, лежащую в основе этой неточности. Настоятельно рекомендуется использовать BigDecimal и BigInteger.

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

BigDecimal a = new BigDecimal(d + 0.1);

Даже если вы пытаетесь использовать BigDecimal здесь, вы все еще выполняете добавление, используя регулярные вычисления с плавающей запятой. Это точно так же, как и делать:

double d_a = d + 0.1; //0.799999999 ad infinitum
BigDecimal a = new BigDecimal(d_a);

Чтобы воспользоваться преимуществами классов BigX, вы должны использовать собственные методы расчета, а также статический метод valueOf (а не конструктор):

BigDecimal a = BigDecimal.valueOf(d).add( BigDecimal.valueOf(0.1) );

Здесь два объекта BigDecimal создаются для соответствия точно 0,7 и 0,1, тогда метод add используется для вычисления их суммы и создания третьего BigDecimal (который будет 0,8 точно).

Использование статического метода valueOf вместо конструктора гарантирует, что созданный объект BigDecimal представляет точное значение double, как оно показано при преобразовании в строку (0,7 в виде строки "0,7" ), а не приблизительное значение, хранящееся на компьютере для его представления (компьютер хранит 0,7 как 0,699999999 до бесконечности).

Ответ 2

(Если вас не интересует теория, прокрутите до конца, это исправление для вашего кода)

Причина довольно проста: как вы знаете, двоичная система поддерживает только 0 и 1 s

Итак, давайте посмотрим на ваши значения и то, что они находятся в двоичном представлении:

0.1 - 0.0001100110011001100110011001100110011001100110011001101
0.2 - 0.001100110011001100110011001100110011001100110011001101
0.3 - 0.010011001100110011001100110011001100110011001100110011
0.4 - 0.01100110011001100110011001100110011001100110011001101
0.5 - 0.1
0.6 - 0.10011001100110011001100110011001100110011001100110011
0.7 - 0.1011001100110011001100110011001100110011001100110011
0.8 - 0.1100110011001100110011001100110011001100110011001101
0.9 - 0.11100110011001100110011001100110011001100110011001101

Что это значит? 0.1 - это 10-й из 1. Нет ничего сложного в десятичной системе, просто сдвиньте разделитель на одну позицию. Но в двоичном режиме вы не можете выражать 0,1 - вызывать каждый сдвиг десятичного знака равным *2 или /2 - в зависимости от направления. (И 10 нельзя разделить на X сдвигов 2)

Для значений, которые вы хотите разделить на кратные 2 - вы получите ТОЧНЫЙ результат:

1/2 - 0.1
1/4 - 0.01
1/8 - 0.001
1/16- 0.0001
and so on.

Поэтому попытка вычисления a /10 представляет собой бесконечный длинный результат, который усекается, когда значение заканчивается из битов.

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

Замечание по сайту: этот "факт" был проигнорирован с помощью системы Patriot, из-за которой он стал непригодным после нескольких часов работы, см. здесь: http://sydney.edu.au/engineering/it/~alum/patriot_bug.html


Но почему это работает на все, кроме 0,7 + 0,1 - вы можете спросить

Если вы тестируете свой код с помощью 0.8 - он работает, но не с 0.7 + 0.1.

Опять же, в двоичном формате оба значения уже неточны. Если вы суммируете оба значения, результат еще более неточна, что приводит к ошибочному результату:

Если вы суммируете 0,7 и 0,1 (после десятичного разделителя), вы получите следующее:

  0.101100110011001100110011001100110011001100110011001 1000
+ 0.000110011001100110011001100110011001100110011001100 1101
  ---------------------------------------------------------
  0.110011001100110011001100110011001100110011001100110 0101     

Но 0,8 будет

  0.110011001100110011001100110011001100110011001100110 1000

Сравните последние 4 бита и отметьте, что результат "0.8" ADDITION меньше, чем если бы вы преобразовали 0.8 в двоичный файл напрямую.

Угадайте, что:

System.out.println(0.7 + 0.1 == 0.8); //returns false

При работе с числами вы должны установить предел точности - и ВСЕГДА округлите числа, чтобы избежать таких ошибок (не усекать!):

 //compare doubles with 3 decimals
 System.out.println((lim(0.7, 3) + lim(0.1, 3)) == lim(0.8, 3)); //true

 public static long lim(double d, int t){
     return Math.round(d*10*t);
 }

Чтобы ваш код был исправлен: округлите его до 4 цифр, прежде чем обрезать после первой цифры:

public static double truncate(double x){
   long y = (long)((Math.round(x*10000)/10000.0)*10);
   double z = (double)y/10;
   return z;
}

System.out.println(truncate(0.7+0.1)); //0.8
System.out.println(truncate(0.8)); //0.8

Это по-прежнему будет усекаться по желанию, но гарантирует, что a 0.69999 будет округлено до 0.7, прежде чем обрезать его. Вы можете установить точность, необходимую для вашего приложения. 10, 20, 30, 40 цифр?

Другие значения будут по-прежнему оставаться правильными, потому что что-то вроде 0.58999 будет округлено до 0,59, поэтому все равно усекается как 0.5 и не округляется до 0.6

Ответ 3

Это не вызвано вашей программой или Java. Просто цифры с плавающей запятой неточны по дизайну. Вы не узнаете, какие цифры будут неточными, но некоторые будут (0,7 в вашем случае). Одна из многих статей на эту тему: http://effbot.org/pyfaq/why-are-floating-point-calculations-so-inaccurate.htm

Итог: никогда не верьте, что double 0.7 ДЕЙСТВИТЕЛЬНО 0.7.

Ответ 4

"Проблема" заключается в использовании чисел с плавающей запятой, таких как double или float. Эти числа используют базовые 2 фракции внутри, чтобы приблизиться к большому диапазону чисел, от очень малого до очень большого.

Так, например, 0.5 может быть представлен как 1/2 и 0.75 как 1\2 + 1\4.

Однако, как вы обнаружили, вы не всегда можете легко преобразовать между фракциями базы 10 и фракциями основания 2.

Где в основании 10 0.7 равно 7/10, в фракции 2 основания это становится очень трудным.

Это похоже на попытку точно представить 1/3 в десятизначном числе базы 10, что очень просто в базовой дроби 3, вы можете получить очень близкое приближение, если у вас достаточно десятичных мест, однако вы не может точно представить 1/3 в десятичном значении базы.

Ответ 5

Ключевым моментом является то, что float и double предназначены для работы с философией округления. Если результат вычисления не может быть представлен точно, результат будет как можно ближе к точному ответу, независимо от того, делает он его меньшим или большим, чем точным.

Задача с 0.7 + 0.1 заключается в том, что 0,7999999999999999993338661852249060757458209991455078125, самое близкое представляемое значение к сумме 0,699999999999999999555910790149937383830547332763671875 и 0,1000000000000000055511151231257827021181583404541015625, представляемые числа, наиболее близкие к 0,7 и 0,1, немного меньше 0,8.

Существует несколько возможных решений. Если десятичная усечка является центральной для проблемы и важнее производительности и пространства, используйте BigDecimal. Если нет, подумайте о добавлении небольшой корректировки для учета этого эффекта перед усечением. По сути дела, обрабатывают числа, очень немного меньшие 0,8, как более или равные 0,8. Это может работать, потому что различия, возникающие при арифметике double, обычно намного меньше различий, которые возникают и имеют значение в реальном мире.

Ответ 6

Java использует формат IEEE 754 для кодирования двойных значений.

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

0,7 наилучшим образом приближается к 0,699999999999999999999999

Проверьте это: http://www.binaryconvert.com/result_double.html?decimal=048046055

Чтобы устранить проблему, можете ли вы попробовать умножить на 10.0 и соответственно обработать свои значения?