Число с плавающей точкой в Java (float и double) хранится по стандарту IEEE 754 (JLS, раздел 4.2.3). У float 32 бита делятся на знак (1 бит), экспоненту (8 бит со смещением 127) и мантиссу (23 бита плюс неявная ведущая единица); у double 64 бита: 1, 11 (смещение 1023) и 52. Значение считается как (-1)знак × 1,мантисса × 2экспонента - 127. Ниже разбор по битам на примере числа 7,5, у которого биты 0 | 10000001 | 11100000000000000000000, то есть 0x40F00000.

Кратко

  • float это 32 бита по IEEE 754: 1 бит знака, 8 бит экспоненты, 23 бита мантиссы; double: 1, 11 и 52 бита.
  • Экспонента хранится со смещением: из восьмибитного числа вычитаем 127 (у double 1023). Коды 00000000 и 11111111 зарезервированы: ноль и денормализованные числа, бесконечность и NaN.
  • Перед мантиссой подразумевается скрытая единица, поэтому реальная точность float 24 бита, а не 23.
  • Значение это (-1)знак × 1,мантисса × 2степень; пример статьи: 0 10000001 11100000000000000000000 это 1,875 × 22 = 7,5.
  • Дроби вроде 0,1 и 0,2 в двоичной системе точно не представимы, поэтому 0.1 + 0.2 == 0.3 в Java ложно; для денег используют BigDecimal.
  • Биты числа в Java показывает Float.floatToIntBits, обратно собирает Float.intBitsToFloat; с Java 17 все вычисления с плавающей точкой строго по IEEE 754.

Содержание:

Заставка: сплошное поле цифр, в котором жирным выделены отдельные фрагменты
Изображение: http://pikabu.ru/

Вступление

В первые же дни изучения Java я наткнулся на такой любопытный вид примитивов, как числа с плавающей точкой. Меня сразу заинтересовали их особенности и, тем более, способ записи в двоичном коде (что взаимосвязано). В отличие от какого-либо диапазона целых чисел, даже в очень малом промежутке (например от 1 до 2) их бесконечное множество. И имея конечный размер памяти, невозможно выразить это множество. Так как же они выражены в двоичном коде и как работают? Увы, объяснения в вики и достаточно клевой статьи на хабре тут не дали мне полного понимания, хотя заложили базу. Осознание пришло лишь после одной статьи-разбора с сайта softelectro.ru (сейчас сайт недоступен) наутро после прочтения.

Экскурс в историю

(почерпнул из этой статьи на Хабре) В 60-70 гг, когда компьютеры были большими, а программы маленькими, еще не было единого стандарта вычислений, как и стандарта выражения самого числа с плавающей точкой. Каждый компьютер делал это по-своему, и ошибки были у каждого свои. Но в середине 70-х компания Intel решила сделать новые процессоры с поддерживаемой "улучшенной" арифметикой и заодно стандартизировать ее. Для разработки привлекли профессоров Уильяма Кэхэна и Джона Палмера (нет, не автора книг про пиво). Не обошлось без драм, но все же новый стандарт был разработан. Сейчас этот стандарт называют IEEE 754: первая редакция принята в 1985 году, действующая в 2019-м. Java следует ему с первой версии, с Java 15 язык опирается на редакцию 2019 года (JLS, раздел 4.2.3), а с Java 17 все вычисления с плавающей точкой всегда строгие по стандарту (JEP 306), поэтому модификатор strictfp больше не нужен.

Формат записи числа с плавающей точкой

Еще в учебниках школьного курса все сталкивались с непривычным способом записи очень больших или очень малых чисел вида 1,2×103 или 1,2E3, что равно 1,2×1000 = 1200. Это называется способ записи через экспоненту. В данном случае мы имеем дело с выражением числа по формуле: N=M×np, где
  • N = 1200: получаемое число
  • M = 1,2: мантисса, дробная часть без учета порядков
  • n = 10: основание порядка. В данном случае и когда речь не идет о компьютерах, основанием выступает число 10
  • p = 3: степень основания
Довольно часто основание порядка подразумевается, как 10 и записывают лишь мантиссу и значение степени основания, разделяя их буквой E. В нашем примере я привел равнозначные записи 1,2×103 и 1,2E3 Если все понятно, и ностальгический экскурс в школьную программу мы закончили, то сейчас рекомендую забыть это, т. к. при формировании числа с плавающей точкой мы имеем дело со степенями двойки, а не десятки, т.е. n = 2, вся стройная формула 1,2E3 ломается и это здорово сломало мне мозг.

Знак и степень

И что мы имеем? В итоге мы также имеем двоичное число, которое состоит из мантиссы, части, которую будем возводить в степень, и самой степени. Кроме этого, так же как принято и у целочисленных типов, в числах с плавающей точкой есть бит, который определяет знак: будет число положительным или отрицательным. В качестве примера предлагаю рассмотреть тип float, который состоит из 32 бит. С числами двойной точности double логика такая же, только бит 64: 1 на знак, 11 на экспоненту (со смещением 1023 вместо 127) и 52 на мантиссу. Из 32 бит первый, старший, отводится на знак, следующие 8 бит отводятся на экспоненту, степень, на которую будем возводить мантиссу, а остальные 23 бита на мантиссу.

float и double: сравнение форматов

Чтобы дальше не путаться, сведем оба формата в одну таблицу:
Параметр float double
Всего бит 32 64
Знак 1 бит 1 бит
Экспонента 8 бит, смещение 127 11 бит, смещение 1023
Мантисса 23 бита плюс скрытая единица, итого 24 значащих 52 бита плюс скрытая единица, итого 53 значащих
Точность около 7 десятичных цифр около 15-16 десятичных цифр
Максимум Float.MAX_VALUE, примерно 3,4×1038 Double.MAX_VALUE, примерно 1,8×10308
Литерал в Java 7.5f 7.5 (без суффикса всегда double)
Для демонстрации давайте посмотрим на пример: Схема 32 бит числа float: бит знака 0, восемь бит экспоненты 10000001, 23 бита мантиссы 11100000000000000000000С первым битом все очень просто. Если значение первого бита 0, то число, которое мы получим будет положительным. Если бит равен 1, то число будет отрицательным. Следующий блок из 8 бит это блок с экспонентой. Экспонента записывается как обычное восьмибитное число, а чтоб получить требуемую степень нам нужно из полученного числа вычесть 127 (это число называют смещением экспоненты). В нашем случае восемь бит экспоненты это 10000001. Это соответствует числу 129. Если есть вопрос, как это посчитать, то на картинке быстрый ответ. Развернутый можно получить на любом курсе булевой алгебры. Таблица перевода экспоненты 10000001 в десятичное число: под каждым битом степень двойки от 128 до 1, в сумме 128 + 1 = 1291×27 + 0×26 + 0×25 + 0×24 + 0×23 + 0×22 + 0×21 + 1×20 = 1×128 + 1×1 = 128+1=129 Не сложно посчитать, что максимальное число, которое мы можем получить из этих 8 бит 111111112 = 25510 (подстрочные 2 и 10 означают двоичную и десятеричную системы исчисления) Однако, если использовать только положительные значения степени (от 0 и до 255), то полученные числа будут иметь много чисел перед запятой, но не после? Чтоб получать отрицательные значения степени, из сформированной экспоненты нужно вычитать 127. Таким образом, арифметически диапазон степеней получается от -127 до 128. Но два крайних кода экспоненты зарезервированы: 00000000 означает ноль и денормализованные числа, а 11111111 бесконечность и NaN, поэтому у обычных чисел степень лежит от -126 до 127. Если использовать наш пример, то нужная степень будет 129-127 = 2. Пока запоминаем это число.

Мантисса

Теперь о мантиссе. Она состоит из 23 бит, однако в начале всегда подразумевается еще одна единица, на которую биты не выделяются. Это сделано из соображений целесообразности и экономии. Одно и то же число можно выражать разными степенями, добавляя к мантиссе нули перед или после запятой. Проще всего это понять с десятичной экспонентой: 120 000 = 1,2×105 = 0,12×106 = 0,012×107 = 0,0012×108 и т.д. Однако, введя фиксированное число в голове мантиссы мы каждый раз будем получать новые числа. Примем как данность, что перед нашими 23 битами будет еще один с единицей. Обычно этот бит от остальных отделяют точкой, которая, впрочем, ничего не значит. Просто так удобнее. 1 . 11100000000000000000000 Мантисса со скрытым битом: единица, точка и 23 бита 11100000000000000000000Теперь полученную мантиссу нужно возводить в степень слева направо, уменьшая с каждым шагом степень на одну. Стартуем со значения степени, которое мы получили в результате вычисления, т. е. 2. (Я специально выбрал простой пример, чтоб не писать каждое значение степени двойки и в приведенной таблице не вычислял их, когда соответствующий бит равен нулю) Таблица вычисления мантиссы: биты 1, 1, 1, 1 с весами 4, 2, 1 и 0,5 дают 7,51×22 + 1×21 + 1×20 + 1×2-1 = 1×4 + 1×2 + 1×1 + 1×0,5 = 4+2+1+0,5 = 7,5 и получили результат 7,5, правильность можно проверить, например, по этой ссылке, а можно и прямо в Java: метод Float.floatToIntBits отдает те же 32 бита в виде int:
int bits = Float.floatToIntBits(7.5f);
System.out.println(Integer.toBinaryString(bits)); // 32 бита, ведущий ноль знака не печатается
System.out.println(Integer.toHexString(bits));    // те же биты в шестнадцатеричном виде
System.out.println(Float.intBitsToFloat(0x40F00000)); // собираем число обратно из битов
Вывод:
1000000111100000000000000000000
40f00000
7.5
Первая строка это наши биты без ведущего нуля знака: сначала экспонента 10000001, затем мантисса 111 и двадцать нулей. Вторая строка это те же биты в шестнадцатеричном виде, а третья это число, собранное из битов обратно.

Итоги

Стандартное число с плавающей точкой типа float состоит из 32 бит: первый бит это знак (+ или -), следующие восемь экспонента, следующие 23 мантисса. По знаку: если бит 0, число положительное. Если бит 1, отрицательное. По экспоненте: побитно переводим в десятичное число (веса битов слева направо: 128, 64, 32, 16, 8, 4, 2, 1), из полученного числа вычитаем 127, получаем степень, с которой будем стартовать. По мантиссе: к имеющимся 23 битам спереди дописываем еще один бит со значением 1 и с него начинаем возводить в полученную нами степень, с каждым следующим битом декрементируя эту степень. That's all folks, kids! Заставка «That's all Folks!» на красных концентрических кругах, как в финале мультфильмов Looney TunesP. S.: В виде домашнего задания, используя эту статью, оставьте в комментариях свои версии, почему при большом количестве арифметических операций с числами с плавающей точкой возникают ошибки точности

Вопросы и ответы

Как число float хранится в памяти?

В 32 битах по стандарту IEEE 754: 1 бит знака, 8 бит экспоненты и 23 бита мантиссы. Значение считается как (-1)знак × 1,мантисса × 2экспонента - 127. Для 7,5 это 0 | 10000001 | 11100000000000000000000: знак плюс, степень 129 - 127 = 2, мантисса 1,111 в двоичной записи, то есть 1,875, и 1,875 × 4 = 7,5. В шестнадцатеричном виде эти же биты записываются как 0x40F00000.

Зачем из экспоненты вычитают 127?

Чтобы хранить отрицательные степени без отдельного бита знака. Восемь бит дают числа от 0 до 255, а вычитание смещения 127 превращает их в степени от -126 до 127 (крайние коды 0 и 255 зарезервированы под ноль, денормализованные числа, бесконечность и NaN). У double смещение 1023 при 11 битах экспоненты. Побочный плюс такого хранения: положительные float можно сравнивать как обычные целые числа.

Что такое скрытый бит мантиссы?

У нормализованного двоичного числа перед запятой всегда стоит единица, как в записи 1,2E3 первая цифра не ноль, поэтому хранить ее не нужно. float хранит 23 бита после запятой, единица подразумевается, и реальная точность равна 24 битам. Исключение: денормализованные числа с экспонентой 00000000, у них вместо скрытой единицы подразумевается ноль, так они дотягиваются до очень малых значений около нуля.

Чем float отличается от double?

Размером и точностью. float: 32 бита, 8 бит экспоненты, 24 значащих бита, около 7 верных десятичных цифр, максимум примерно 3,4×1038. double: 64 бита, 11 бит экспоненты, 53 значащих бита, около 15-16 цифр, максимум примерно 1,8×10308. В Java литерал без суффикса (7.5) имеет тип double, для float нужен суффикс f. По умолчанию берите double: он точнее, а на современных процессорах не медленнее.

Почему 0.1 + 0.2 не равно 0.3 в Java?

Потому что 0,1 и 0,2 не представимы точно в двоичной системе, как 1/3 в десятичной: в double они хранятся с ошибкой округления, и сумма печатается как 0.30000000000000004. Сравнивать вещественные числа через == нельзя: сравнивайте разность с допуском (Math.abs(a - b) < 1e-9) или используйте BigDecimal для денег. Это же и подсказка к домашнему заданию автора: ошибка каждого округления накапливается с каждой операцией.

Как посмотреть биты числа float в Java?

Метод Float.floatToIntBits(7.5f) возвращает int с теми же 32 битами, а Integer.toBinaryString их печатает: 1000000111100000000000000000000 (ведущий нулевой бит знака опущен). Обратно: Float.intBitsToFloat(0x40F00000) вернет 7.5. Для double есть пара Double.doubleToLongBits и Double.longBitsToDouble плюс Long.toBinaryString.

Читайте также