JavaRush /Java Blog /Random-KO /Устройство вещественных чисел

Устройство вещественных чисел

Random-KO 그룹에 게시되었습니다
Hello! В сегодняшней лекции расскажем о числах в Java, а конкретно — о вещественных числах. Устройство вещественных чисел - 1Без паники! :) НиHowих математических сложностей в лекции не будет. Будем говорить о вещественных числах исключительно с нашей, «программистской» точки зрения. Итак, что же такое «вещественные числа»? Вещественные числа — это числа, у которых есть дробная часть (она может быть нулевой). Они могут быть положительными or отрицательными. Вот несколько примеров: 15 56.22 0.0 1242342343445246 -232336.11 Как же устроено вещественное число? Достаточно просто: оно состоит из целой части, дробной части и знака. У положительных чисел знак обычно не указывают явно, а у отрицательных указывают. Ранее мы подробно разобрали, Howие операции над числами можно совершать в Java. Среди них было много стандартных математических операций — сложение, вычитание и т. д. Было и кое-что новое для тебя: например, остаток от деления. Но How именно устроена работа с числами внутри компьютера? В Howом виде они хранятся в памяти?

Хранение вещественных чисел в памяти

Думаю, для тебя не станет открытием, что числа бывают большими и маленькими :) Их можно сравнивать друг с другом. Например, число 100 меньше числа 423324. Влияет ли это на работу компьютера и нашей программы? На самом деле — да. Каждое число представлено в Java определенным диапазоном значений:
Тип Размер в памяти (бит) Диапазон значений
byte 8 бит от -128 до 127
short 16 бит от -32768 до 32767
char 16 бит беззнаковое целое число, которое преставляет собой символ UTF-16 (буквы и цифры)
int 32 бита от -2147483648 до 2147483647
long 64 бита от -9223372036854775808 до 9223372036854775807
float 32 бита от 2-149 до (2-2-23)*2127
double 64 бита от 2-1074 до (2-2-52)*21023
Сегодня поговорим именно о последних двух типах — float и double. Оба выполняют одну и ту же задачу — представляют дробные числа. Их еще очень часто называют «числа с плавающей точкой». Запомни этот термин на будущее :) Например, число 2.3333 or 134.1212121212. Довольно странно. Ведь получается, нет ниHowой разницы между этими двумя типами, раз они выполняют одну и ту же задачу? Но разница есть. Обрати внимание на столбец «размер в памяти» в таблице выше. Все числа (да и не только числа — вообще вся информация) хранится в памяти компьютера в виде битов. Бит — это самая маленькая единица измерения информации. Она довольно проста. Любой бит equals or 0, or 1. Да и само слово «bit» происходит от английского «binary digit» — двоичное число. Думаю, ты наверняка слышал о существовании двоичной системы счисления в математике. Любое привычное нам десятичное число можно представить в виде набора единиц и нулей. Например, число 584.32 в двоичной системе будет выглядеть так: 100100100001010001111. Каждые единица и ноль в этом числе являются отдельным битом. Теперь тебе должна быть более понятна разница между типами данных. Например, если мы создаем число типа float, в нашем распоряжении есть всего 32 бита. При создании числа float именно столько места будет выделено для него в памяти компьютера. Если же мы хотим создать число 123456789.65656565656565, в двоичном виде оно будет выглядеть так: 11101011011110011010001010110101000000. Оно состоит из 38 единиц и нулей, то есть для его хранения в памяти нужно 38 бит. В тип float это число просто не «влезет»! Поэтому число 123456789 можно представить в виде типа double. Для его хранения выделяется целых 64 бита: это нам подходит! Разумеется, и диапазон значений тоже будет подходящим. Для удобства ты можешь представлять число How маленький ящик с ячейками. Если ячеек хватает для хранения каждого бита, значит, тип данных выбран правильно :) Устройство вещественных чисел - 2Разумеется, разное количество выделяемой памяти влияет и на само число. Обрати внимание, что у типов float и double отличается диапазон значений. What это означает на практике? Число double может выразить большую точность, чем число float. У 32-битных чисел с плавающей точкой (в Java это How раз тип float) точность составляет примерно 24 бита, то есть около 7 знаков после запятой. А у 64-битных чисел (в Java это тип double) — точность примерно 53 бита, то есть примерно 16 знаков после запятой. Вот пример, который хорошо демонстрирует эту разницу:

public class Main {

   public static void main(String[] args)  {

       float f = 0.0f;
       for (int i=1; i <= 7; i++) {
           f += 0.1111111111111111;
       }

       System.out.println(f);
   }
}
What мы должны получить здесь в качестве результата? Казалось бы, все довольно просто. У нас есть число 0.0, и мы 7 раз подряд прибавляем к нему 0.1111111111111111. В итоге должно получиться 0.7777777777777777. Но мы создали число float. Его размер ограничен 32 битами и, How мы сказали ранее, он способен отобразить число примерно до 7 знака после запятой. Поэтому в итоге результат, который мы получим в консоли, будет отличаться от того, что мы ожидали:

0.7777778
Число How будто было «обрезано». Ты уже знаешь How хранятся данные в памяти — в виде битов, поэтому тебя не должно это удивлять. Понятно, почему это произошло: результат 0.7777777777777777 просто не влез в выделенные нам 32 бита, поэтому и был обрезан так, чтобы поместиться в переменную типа float :) Мы можем изменить тип переменной на double в нашем примере, и тогда итоговый результат не будет обрезан:

public class Main {

   public static void main(String[] args)  {

       double f = 0.0;
       for (int i=1; i <= 7; i++) {
           f += 0.1111111111111111;
       }

       System.out.println(f);
   }
}

0.7777777777777779
Здесь уже 16 знаков после запятой, результат «уместился» в 64 бита. Кстати, возможно ты заметил, что в обоих случаях результаты получorсь не совсем корректными? Подсчет был произведен с небольшими ошибками. О причинах этого мы поговорим ниже :) Теперь скажем пару слов о том, How можно сравнить числа между собой.

Сравнение вещественных чисел

Мы частично уже затрагивали этот вопрос в прошлой лекции, когда говорor об операциях сравнения. Такие операции How >, <, >=, <= повторно разбирать мы не будем. Вместо этого рассмотрим более интересный пример:

public class Main {

   public static void main(String[] args)  {

       double f = 0.0;
       for (int i=1; i <= 10; i++) {
           f += 0.1;
       }

       System.out.println(f);
   }
}
Как ты думаешь, Howое число будет выведено на экран? Логичным ответом был бы ответ: число 1. Мы начинаем отсчет с числа 0.0 и последовательно прибавляем к нему 0.1 десять раз подряд. Вроде все правильно, должна получиться единица. Попробуй запустить этот code, и ответ сильно тебя удивит :) Вывод в консоль:

0.9999999999999999
Но почему в таком простом примере возникла ошибка? О_о Тут бы даже пятиклассник с легкостью верно ответил, но программа на Java выдала неточный результат. «Неточный» тут более подходящее слово, чем «неправильный». Мы все-таки получor очень близкое к единице число, а не просто Howое-то рандомное meaning :) Оно отличается от правильного буквально на миллиметр. Но почему? Возможно, это просто разовая ошибка. Может, комп заглючил? Попробуем написать другой пример.

public class Main {

   public static void main(String[] args)  {

       //add 0.1 to zero eleven times in a row
       double f1 = 0.0;
       for (int i = 1; i <= 11; i++) {
           f1 += .1;
       }

       // Multiply 0.1 by 11
       double f2 = 0.1 * 11;

       //should be the same - 1.1 in both cases
       System.out.println("f1 = " + f1);
       System.out.println("f2 = " + f2);

       // Let's check!
       if (f1 == f2)
           System.out.println("f1 and f2 are equal!");
       else
           System.out.println("f1 and f2 are not equal!");
   }
}
Вывод в консоль:

f1 = 1.0999999999999999
f2 = 1.1
f1 и f2 не равны!
Так, дело явно не в глюках компа :) What происходит? Подобные ошибки связаны с тем, How числа представлены в двоичном виде в памяти компьютера. Дело в том, что в двоичной системе невозможно точно представить число 0,1. В десятичной системе, кстати, тоже есть подобная проблема: в ней нельзя правильно представить дроби (и instead of ⅓ мы получим 0.33333333333333…, что тоже не совсем правильный результат). Казалось бы, мелочь: при таких подсчетах разница может быть в одну стотысячную часть (0,00001) or даже меньше. Но что, если от этого сравнения будет зависеть весь результат работы твоей Очень Серьезной Программы?

if (f1 == f2)
   System.out.println("Rocket flies into space");
else
   System.out.println("The launch is canceled, everyone goes home");
Мы явно ожидали, что два числа будут равны, но из-за особенностей внутреннего устройства памяти мы отменor запуск ракеты. Устройство вещественных чисел - 3Раз так, нам нужно определиться, How же все-таки сравнить два числа с плавающей точкой, чтобы результат сравнения был более...эммм...предсказуемым. Итак, правило №1 при сравнении вещественных чисел мы уже усвоor: никогда не используй == при сравнении чисел с плавающей точкой. Ок, плохих примеров, думаю, достаточно :) Давай рассмотрим хороший пример!

public class Main {

   public static void main(String[] args)  {

       final double threshold = 0.0001;

       //add 0.1 to zero eleven times in a row
       double f1 = .0;
       for (int i = 1; i <= 11; i++) {
           f1 += .1;
       }

       // Multiply 0.1 by 11
       double f2 = .1 * 11;

       System.out.println("f1 = " + f1);
       System.out.println("f2 = " + f2);

       if (Math.abs(f1 - f2) < threshold)
           System.out.println("f1 and f2 are equal");
       else
           System.out.println("f1 and f2 are not equal");
   }
}
Здесь мы по сути делаем то же самое, но меняем способ сравнения чисел. У нас есть специальное «пороговое» число — 0.0001, одна десятитысячная. Оно может быть и другим. Это зависит от того, насколько точное сравнение тебе нужно в конкретном случае. Можно сделать его и больше, и меньше. С помощью метода Math.abs() мы получаем модуль числа. Модуль — это meaning числа независимо от знака. Например, у чисел -5 и 5 модуль будет одинаковым и equals 5. Мы вычитаем второе число из первого, и если полученный результат, независимо от знака, будет меньше того порога, который мы установor, значит наши числа равны. Во всяком случае, они равны до той степени точности, которую мы установor с помощью нашего «порогового числа», то есть How минимум они равны вплоть до одной десятитысячной. Такой способ сравнения избавит тебя от неожиданного поведения, которое мы увидели в случае с ==. Еще один хороший способ сравнения вещественных чисел — использовать специальный класс BigDecimal. Этот класс специально был создан для хранения очень больших чисел с дробной частью. В отличие от double и float, при использовании BigDecimal сложение, вычитание и прочие математические операции выполняются не с помощью операторов (+- и т.д.), а с помощью методов. Вот How это будет выглядеть в нашем случае:

import java.math.BigDecimal;

public class Main {

   public static void main(String[] args)  {

       /*Create two BigDecimal objects - zero and 0.1.
       We do the same thing as before - add 0.1 to zero 11 times in a row
       In the BigDecimal class, addition is done using the add () method */
       BigDecimal f1 = new BigDecimal(0.0);
       BigDecimal pointOne = new BigDecimal(0.1);
       for (int i = 1; i <= 11; i++) {
           f1 = f1.add(pointOne);
       }

       /*Nothing has changed here either: create two BigDecimal objects
       and multiply 0.1 by 11
       In the BigDecimal class, multiplication is done using the multiply() method*/
       BigDecimal f2 = new BigDecimal(0.1);
       BigDecimal eleven = new BigDecimal(11);
       f2 = f2.multiply(eleven);

       System.out.println("f1 = " + f1);
       System.out.println("f2 = " + f2);

       /*Another feature of BigDecimal is that number objects need to be compared with each other
       using the special compareTo() method*/
       if (f1.compareTo(f2) == 0)
           System.out.println("f1 and f2 are equal");
       else
           System.out.println("f1 and f2 are not equal");
   }
}
Какой же вывод в консоль мы получим?

f1 = 1.1000000000000000610622663543836097232997417449951171875
f2 = 1.1000000000000000610622663543836097232997417449951171875
f1 и f2 равны
Мы получor ровно тот результат, на который рассчитывали. И обрати внимание, насколько точными получorсь наши числа, и сколько знаков после запятой в них уместилось! Гораздо больше, чем во float и даже в double! Запомни класс BigDecimal на будущее, он тебе обязательно пригодится :) Фух! Лекция получилась немаленькая, но ты справился: молодец! :) Увидимся на следующем занятии, будущий программист!
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION