19 апр. 2015 г.

Примитивные вещественные типы - знакомство с граблями

GrabliПри работе с вещественными типами в Java есть несколько граблей, часть из них, не самые страшные, уже были упомянуты в предыдущей статье.

Теперь рассмотрим несколько других, наиболее интересных :) Которые не часто освещаются в книга по программированию на Java и соответственно часто вызывают недоумения у начинающих программистов и не только на Java, поскольку это связано с представлением чисел с плавающей точкой в процессорах.

Чтобы сразу стало понятно о чём пойдет речь, разберем простой пример. Как вы уже должны знать, вещественные литералы в Java, по умолчанию имеют тип double. Теперь допустим у нас есть вот такая строка:

double d = 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1;

То есть мы 10 раз складываем число 0.1. Можно предположить что в результате получится 1, но как бы не так. Мы получим число очень близкое к единице но не единицу, например это может быть число:

0.9999999999999999

Если же мы этот же эксперимент проделаем для типа float:

float f = 0.1f + 0.1f + 0.1f + 0.1f + 0.1f + 0.1f + 0.1f + 0.1f + 0.1f + 0.1f;

то там будет другой результат:

1.0000001

Это объясняется разностью в точности представления вещественных чисел в типах double и float в двоичном формате (вспоминаем, что компьютер работает только с нулями и единицами :) ).

Более подробно и популярно почему так происходит можно почитать тут и тут. Чистая теори о представлении вещественных чисел тут.

Из этого следует, что вещественные числа обычно не сравнивают оператором ==, а проверяют что модуль разности двух чисел меньше какого-то маленького числа (погрешности вычислений).

Еще одними грабельками при вычислениях с вещественными числами может быть ситуация когда возможно подобрать такое положительное число Х при котором будет верно сравнение a+x == x, то есть прибавление какого-то числа к переменной не меняет ее значения.

Чтобы стало все понятнее приведу пример:

import static pro.java.util.Print.*;
public class FloatingPoint02 {
public static void main(String[] args) {
double d = 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1;
float f = 0.1f + 0.1f + 0.1f + 0.1f + 0.1f + 0.1f + 0.1f + 0.1f + 0.1f + 0.1f;
println("d= " + d);
println("f= " + f);
println("---------");
println("0.35-0.33 = " + (0.35 + 0.33));
println("---------");
double d01 = 0.1;
double d00 = 0;
for (int i = 0; i < 10; i++) {
d00 += d01;
println("Итерация " + (i + 1) + " " + d00);
}
println("---------");
double d1 = .1;
double d2 = .2;
double d3 = .3;
println("d1 = " + d1 + " d2= " + d2 + " d3= " + d3);
println("Прямое сравнение");
println("d1 + d2 = d3 " + ((d1 + d2) == d3));
println("Проверка на допустимую погрешность – 0.0001");
println("d1 + d2 = d3 " + (Math.abs(d1 + d2 - d3) < 1E-4));
double dOne = 1.0;
int count = 0;
println("Пример когда d+x == d");
for (double dd = 0; dd <= 4.9E-323; dd = Math.nextUp(dd)){
if ((dOne + dd)== dOne ){ // вот это интересно :)
println("dd = " + dd);
count++;
}
}
println("Итого в заданном промежутке есть " + count + " чисел");
println("при суммировании с которыми dOne будет равно dOne :)");
double dPlus = 4.9E-324;
println("Например dPlus = " + dPlus + " dOne = " + dOne);
println("dOne + dPlus = " + (dPlus + dOne));
dOne = dOne + dPlus;
println("dOne = dOne + dPlus и после этого dOne == " + dOne);
}
}

Вывод программы:

FP0002

FP0003Приведу еще один интересный пример слева и его вывод ниже:

FP0004Как видим, в первом случае dd и ff не равны друг другу. Такое происходит при сужающем преобразовании.

Затем мы сделали наоборот и теперь dd равно ff. Весело, не правда ли? :)

 

Именно из-за такого поведения double и float не используют в финансовых расчётах.

И еще парочка статей на эту тему: раз и два.

Ну и на последок, очень хорошая статья на эту тему. А BigInteger и BigDecimal, рассматриваемые в ней, мы изучим чуть позже.

Комментариев нет:

Отправить комментарий