Особенности представления чисел в ЭВМ
Вещественные числа
Обсуждаемые в данной секции вопросы значительно более полно рассмотрены в четвертой главе классической книги Кнута [7].
Для представления вещественных чисел в языке Java используют переменные и константы типов float и double. Величины первого из них занимают четыре байта, а второго — восемь.
В отличие от множества целых чисел вещественные
числа
обладают свойством полноты: между любыми двумя
различными
числами всегда найдется отличное от них третье. Понятно, что любой из
способов представления бесконечного множества вещественных чисел с помощью
некоторого конечного множества
не даст возможности
сохранить
свойство полноты. Наиболее распространенным способом реализации вещественных
чисел на ЭВМ является использование чисел с плавающей точкой.
При этом множество
оказывается состоящим из
элементов вида
![f = \pm (\frac{\alpha_1}{p}+\frac{\alpha_2}{p^2}+\ldots+
\frac{\alpha_n}{p^n})p^e,](/sites/default/files/tex_cache/345c1e0ce89edb12450ac3af8a635e51.png)
где целые числа для всех
из диапазона от
1 до
удовлетворяют
соотношению
, а величина
лежит
в диапазоне от
до
(
). Число
является при этом основанием системы счисления (чаще всего это 2), константа
задает точность представления чисел (для типа double она
больше,
чем для float ), а диапазон
определяет область
значений
экспоненты (для типа double он также больше, чем для float ).
Число принято называть мантиссой числа
с
плавающей
точкой. Часто требуют, чтобы для всех чисел
величина
была ненулевой. Такие числа с плавающей точкой называют нормализованными.
В языке Java для кодирования величин типов float и double
также используют числа с плавающей точкой. При этом часть из имеющегося
множества бит используют для размещения экспоненты , а
остальные
биты — для размещения мантиссы.
Для того чтобы хорошо понять, что же представляет из себя множество нормализованных чисел с плавающей точкой, полезно
изобразить
его на числовой прямой для случая небольших
,
и
. Подобная задача
приведена в конце параграфа. Сейчас же нам будет достаточно весьма
качественного описания этого множества.
Так как симметрично относительно начала
координат, то можно
разобраться только с неотрицательными числами. Нуль, конечно же, принадлежит
искомому множеству. Ближайшая к нулю следующая точка получается при
,
и
.
Это число для чисел
типов float
и double определено, как MIN_VALUE в классах java.lang.Float и java.lang.Double соответственно.
Правее располагается множество точек, следующих друг за другом с шагом . Затем шаг
увеличивается. Потом еще. И еще. При
расстояние между двумя
соседними
точками множества
достигает
.
Самая правая точка
множества получается при
и
. Для типов float и double это число определено, как MAX_VALUE
в классах java.lang.Float и java.lang.Double.
Кроме перечисленных (и симметричных им отрицательных) значений в результате выполнения некоторых операций могут получиться также следующие особые значения: плюс бесконечность ( POSITIVE\_INFINITY ), минус бесконечность ( NEGATIVE\_INFINITY ), минус ноль и не число ( NaN ). Например, при делении единицы на минус ноль получается минус бесконечность.
Описанные особенности множества машинных вещественных чисел
приводят к тому, что не для всех его элементов верны следующие соотношения,
всегда справедливые для множества настоящих вещественных чисел
:
-
;
-
существует;
-
;
-
;
-
;
-
.
Приведем несколько примеров, иллюстрирующих эти особенности множества .
Задача 4.1. Предъявите действительное (типа double ) число такое, что
, а
. Воспользуйтесь тем, что класс java.lang.Double определяет константу MAX_VALUE.
Текст программы
public class DblMaxVal { public static void main(String[] args) { double x = Double.MAX_VALUE; double y = x + 1.0; if (x == y) { Xterm.print("Для числа "); Xterm.print("x = " + x, Xterm.Blue); Xterm.print(" величины x и (x+1) "); Xterm.print("равны!\n", Xterm.Red); } y = 2.0 * x; double z = y / 2.0; if (x != z) { Xterm.print("Для числа "); Xterm.print("x = " + x, Xterm.Blue); Xterm.print(" величины x и (2.0*x)/2.0 "); Xterm.print("различны\n", Xterm.Red); Xterm.print("и равны соответственно"); Xterm.print(" " + x, Xterm.Red); Xterm.print(" и"); Xterm.print(" " + z + "\n", Xterm.Red); } } }
Задача 4.2.Предъявите такие действительные (типа double ) числа ,
и
такие,
что
.
Достаточно вспомнить, что точки множества
расположены
на числовой прямой неравномерно.
Текст программы
public class DblNoAssociative { public static void main(String[] args) { double x = 1.0e-16; double y = 1. + (x + x); double z = (1. + x) + x ; if (y != z) { Xterm.print("Для числа "); Xterm.print("x = " + x, Xterm.Blue); Xterm.print(" величины 1.+(x+x) и (1.+x)+x "); Xterm.print("различны\n", Xterm.Red); Xterm.print("и равны соответственно"); Xterm.print(" " + y, Xterm.Red); Xterm.print(" и"); Xterm.print(" " + z + "\n", Xterm.Red); } } }
Иногда даже работа с казалось бы не слишком маленькими или огромными по абсолютной величине числами может привести к удивительным результатам. Рассмотрим задачу о решении квадратного уравнения.
Задача Напишите программу, вводящую действительные коэффициенты ,
и
квадратного уравнения
с положительным дискриминантом,
находящую оба корня этого уравнения.
Вот вполне естественное решение этой задачи, которое может быть написано любым человеком, обладающим минимальными знаниями языка Java.
Текст программы
public class SquareEquation { public static void main(String[] args) throws Exception { double a = Xterm.inputDouble("Введите число a -> "); double b = Xterm.inputDouble("Введите число b -> "); double c = Xterm.inputDouble("Введите число c -> "); if (a == 0.) { Xterm.println("Уравнение не квадратное", Xterm.Red); System.exit(0); } if (b*b - 4.*a*c <= 0.) { Xterm.println("Дискриминант неположителен", Xterm.Red); return; } double x1 = ( -b - Math.sqrt(b*b - 4.*a*c) ) / (2.*a); double x2 = ( -b + Math.sqrt(b*b - 4.*a*c) ) / (2.*a); Xterm.println("Корни уравнения:"); Xterm.println("x1 = " + x1, Xterm.Blue); Xterm.println("x2 = " + x2, Xterm.Blue); } }
Для извлечения квадратного корня здесь используется метод sqrt класса Math. Вызов метода System.exit и применение оператора return, которые в теле метода main почти эквивалентны и приводят к немедленному завершению выполнения программы, позволяет обойтись без ветви else оператора if-else. Остальная часть программы комментариев не требует.
Попробуйте, однако, выполнить эту программу при a=0.2E-16 и b=c=1.0 — первый корень уравнения x1 окажется равным -5.0E16, а второй x2 — нулю.
Если подставить эти значения в выражение , то
и для x1 и для x2 его значение окажется равным единице, а не нулю.
Подобная ошибка для первого, весьма большого по величине корня,
представляется вполне естественной, но вот соглашаться с тем, что нуль
является корнем данного уравнения, совсем не хочется.
Этой проблеме посвящена одна из приведенных ниже задач.