
- •Тема 3. Лексические структуры языка. Примитивные типы данных. Декларация и инициализация переменных. Основные типы операторов.
- •3.1 Примитивные типы данных
- •3.2 Лексические структуры языка
- •3.2.1 Пробелы
- •3.2.2 Идентификаторы
- •3.2.3 Константы
- •3.2.4 Комментарии
- •3.2.5 Разделители
- •3.2.6 Ключевые слова Java
- •4 Операторы
- •4.1 Операция присваивания
- •4.2 Унарные операции
- •4.3 Арифметические бинарные операции
- •4.6 Операции сравнения
- •4.6.1 Логические операции
- •4.7 Условная операция
- •4.8 Приоритет операций
- •4.9 Преобразование и приведение типов при выполнении операций
- •4.10 Переполнение целого числа
- •4.11 Операции с дробными типами
- •4.12 Операция конкатенации строк
- •5 Классы-обертки
- •6 Уловки и ловушки, связанные с плавающей точкой и десятичными числами
- •6.1 Плавающая точка ieee
- •6.2 Специальные числа
- •6.3 Непредвиденные обстоятельства использования плавающей точки
- •6.4 Ошибки округления
- •6.5 Рекомендации по сравнению чисел с плавающей точкой
- •6.6 Не используйте числа с плавающей точкой для точных значений
- •6.7 Большие десятичные дроби для маленьких чисел
- •6.8 Все методы сравнения не созданы равными
- •6.9 Используйте BigDecimal в качестве типа обмена
- •6.10 Построение чисел BigDecimal
6.3 Непредвиденные обстоятельства использования плавающей точки
В связи с особым поведением бесконечности, NaN и 0 определенные трансформации и оптимизации могут показаться безвредными, но при применении к числам с плавающей точкой будут приводить к ошибкам. Например, несмотря на то, что равенство 0.0-f и -f кажется очевидным, но оно ложно, если f равно 0. Существуют и другие подобные проблемы, некоторые из которых показаны в таблице 2.
Таблица 2. Ошибочные представления о числах с плавающей точкой
Данное выражение... |
не обязательно тождественно... |
при условии, что... |
0.0 - f |
-f |
f равно 0 |
f < g |
! (f >= g) |
f или g являются NaN |
f == f |
верно |
f является NaN |
f + g - g |
f |
g является бесконечностью или NaN |
6.4 Ошибки округления
Арифметика чисел с плавающей точкой не отличается особой точностью. Тогда как некоторые числа, например, 0.5, можно точно представить как двоично-десятичные (основание 2) (поскольку 0.5 равно 2-1), но другие числа, например, 0.1 - невозможно. В итоге операции над числами с плавающей точкой могут привести к ошибкам округления, выдавая результат, близкий - но не равный - тому результату, который можно было ожидать. Например, простое вычисление, приведенное ниже, равняется 2.600000000000001, а не 2.6:
double s=0;
for (int i=0; i<26; i++)
s += 0.1;
System.out.println(s);
Аналогично, умножение .1*26 выдает результат, отличный от прибавления .1 к самому себе 26 раз. Ошибки округления могут оказаться даже более серьезными при преобразовании типа от вещественного к целому, поскольку при преобразовании к целому типу нецелая часть отбрасывается, даже для вычислений, которые "выглядят похожими", они должны иметь целые значения. Например, следующие выражения:
double d = 29.0 * 0.01;
System.out.println(d);
System.out.println((int) (d * 100));
на выходе дадут:
0.29
28
что не совсем соответствует первоначально ожидаемому результату.
6.5 Рекомендации по сравнению чисел с плавающей точкой
В связи с необычным поведением сравнения NaN и ошибок округления, которые фактически гарантированы практически при всех вычислениях с плавающей точкой, расшифровывание результатов, выдаваемых операторами сравнения значений с плавающей точкой, является весьма запутанным.
Лучше всего вообще стараться избегать сравнений чисел с плавающей точкой. Конечно же, это не всегда возможно, но Вы должны понимать ограничения сравнения чисел с плавающей точкой. Если Вам нужно сравнить числа с плавающей точкой для того, чтобы узнать, тождественны ли они, то вместо этого следует сравнивать абсолютное значение их разности с каким-либо предварительно выбранным значением эпсилон. То есть, таким образом, Вы проверяете насколько они "близки". (Если Вам не известен коэффициент масштабирования основных измерений, то использование проверки "abs(a/b - 1) < эпсилон", вероятно, будет более надежным, чем простое сравнение разности.) Даже проверка значения для того, чтобы узнать, является ли оно большим или меньшим, чем ноль, является рискованной - вычисления, в результате которых "предполагается" получить значение чуть больше нуля, на самом деле могут привести к числам чуть меньше нуля вследствие суммарных ошибок округления.
Неупорядоченность NaN увеличивает вероятность появления ошибок при сравнении чисел с плавающей точкой. Хорошим практическим приемом для предотвращения многих сбоев, связанных с бесконечностью и NaN при сравнении чисел с плавающей точкой, является явная проверка валидности значения, вместо попыток исключения невалидных значений. В листинге 1 присутствуют две возможных реализации схемы настройки для свойства, которое может принимать только неотрицательные значения. В первом случае будет допустим NaN, во втором - нет. Вторая форма является более предпочтительной, так как она явно проверяет ту область значений, которую Вы считаете валидной.
Листинг 1. Лучшие и худшие способы для обеспечения неотрицательного плавающего значения
// Trying to test by exclusion - this doesn't catch NaN or infinity
public void setFoo(float foo) {
if (foo < 0)
throw new IllegalArgumentException(Float.toString(f));
this.foo = foo;
}
// Testing by inclusion - this does catch NaN
public void setFoo(float foo) {
if (foo >= 0 && foo < Float.INFINITY)
this.foo = foo;
else
throw new IllegalArgumentException(Float.toString(f));
}