Привет!
В сегодняшней лекции расскажем о числах в Java, а конкретно — о вещественных числах.
Без паники! :) Никаких математических сложностей в лекции не будет. Будем говорить о вещественных числах исключительно с нашей, «программистской» точки зрения.
Итак, что же такое «вещественные числа»?
Вещественные числа — это числа, у которых есть дробная часть (она может быть нулевой). Они могут быть положительными или отрицательными.
Вот несколько примеров:
15
56.22
0.0
1242342343445246
-232336.11
Как же устроено вещественное число?
Достаточно просто: оно состоит из целой части, дробной части и знака. У положительных чисел знак обычно не указывают явно, а у отрицательных указывают.
Ранее мы подробно разобрали, какие операции над числами можно совершать в Java. Среди них было много стандартных математических операций — сложение, вычитание и т. д. Было и кое-что новое для тебя: например, остаток от деления.
Но как именно устроена работа с числами внутри компьютера? В каком виде они хранятся в памяти?
Хранение вещественных чисел в памяти
Думаю, для тебя не станет открытием, что числа бывают большими и маленькими :) Их можно сравнивать друг с другом. Например, число 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 или 134.1212121212.
Довольно странно. Ведь получается, нет никакой разницы между этими двумя типами, раз они выполняют одну и ту же задачу? Но разница есть. Обрати внимание на столбец «размер в памяти» в таблице выше.
Все числа (да и не только числа — вообще вся информация) хранится в памяти компьютера в виде битов. Бит — это самая маленькая единица измерения информации. Она довольно проста. Любой бит равен или 0, или 1. Да и само слово «bit» происходит от английского «binary digit» — двоичное число.
Думаю, ты наверняка слышал о существовании двоичной системы счисления в математике. Любое привычное нам десятичное число можно представить в виде набора единиц и нулей.
Например, число 584.32 в двоичной системе будет выглядеть так: 100100100001010001111.
Каждые единица и ноль в этом числе являются отдельным битом. Теперь тебе должна быть более понятна разница между типами данных. Например, если мы создаем число типа float, в нашем распоряжении есть всего 32 бита. При создании числа float именно столько места будет выделено для него в памяти компьютера.
Если же мы хотим создать число 123456789.65656565656565, в двоичном виде оно будет выглядеть так:
11101011011110011010001010110101000000.
Оно состоит из 38 единиц и нулей, то есть для его хранения в памяти нужно 38 бит.
В тип float это число просто не «влезет»! Поэтому число 123456789 можно представить в виде типа double. Для его хранения выделяется целых 64 бита: это нам подходит! Разумеется, и диапазон значений тоже будет подходящим.
Для удобства ты можешь представлять число как маленький ящик с ячейками. Если ячеек хватает для хранения каждого бита, значит, тип данных выбран правильно :)
Разумеется, разное количество выделяемой памяти влияет и на само число.
Обрати внимание, что у типов float и double отличается диапазон значений.
Что это означает на практике? Число double может выразить большую точность, чем число float.
У 32-битных чисел с плавающей точкой (в Java это как раз тип 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);
}
}
Что мы должны получить здесь в качестве результата? Казалось бы, все довольно просто.
У нас есть число 0.0, и мы 7 раз подряд прибавляем к нему 0.1111111111111111. В итоге должно получиться 0.7777777777777777.
Но мы создали число float. Его размер ограничен 32 битами и, как мы сказали ранее, он способен отобразить число примерно до 7 знака после запятой. Поэтому в итоге результат, который мы получим в консоли, будет отличаться от того, что мы ожидали:
0.7777778
Число как будто было «обрезано». Ты уже знаешь как хранятся данные в памяти — в виде битов, поэтому тебя не должно это удивлять. Понятно, почему это произошло: результат 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 бита. Кстати, возможно ты заметил, что в обоих случаях результаты получились не совсем корректными? Подсчет был произведен с небольшими ошибками.
О причинах этого мы поговорим ниже :)
Теперь скажем пару слов о том, как можно сравнить числа между собой.
Сравнение вещественных чисел
Мы частично уже затрагивали этот вопрос в прошлой лекции, когда говорили об операциях сравнения. Такие операции как >, <, >=, <= повторно разбирать мы не будем.
Вместо этого рассмотрим более интересный пример:
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);
}
}
Как ты думаешь, какое число будет выведено на экран? Логичным ответом был бы ответ: число 1. Мы начинаем отсчет с числа 0.0 и последовательно прибавляем к нему 0.1 десять раз подряд. Вроде все правильно, должна получиться единица.
Попробуй запустить этот код, и ответ сильно тебя удивит :)
Вывод в консоль:
0.9999999999999999
Но почему в таком простом примере возникла ошибка? О_о
Тут бы даже пятиклассник с легкостью верно ответил, но программа на Java выдала неточный результат.
«Неточный» тут более подходящее слово, чем «неправильный». Мы все-таки получили очень близкое к единице число, а не просто какое-то рандомное значение :)
Оно отличается от правильного буквально на миллиметр. Но почему?
Возможно, это просто разовая ошибка. Может, комп заглючил? Попробуем написать другой пример.
public class Main {
public static void main(String[] args) {
//прибавляем к нулю 0.1 одиннадцать раз подряд
double f1 = 0.0;
for (int i = 1; i <= 11; i++) {
f1 += .1;
}
//Умножаем 0.1 на 11
double f2 = 0.1 * 11;
//должно получиться одно и то же - 1.1 в обоих случаях
System.out.println("f1 = " + f1);
System.out.println("f2 = " + f2);
// Проверим!
if (f1 == f2)
System.out.println("f1 и f2 равны!");
else
System.out.println("f1 и f2 не равны!");
}
}
Вывод в консоль:
f1 = 1.0999999999999999
f2 = 1.1
f1 и f2 не равны!
Так, дело явно не в глюках компа :) Что происходит?
Подобные ошибки связаны с тем, как числа представлены в двоичном виде в памяти компьютера.
Дело в том, что в двоичной системе невозможно точно представить число 0,1. В десятичной системе, кстати, тоже есть подобная проблема: в ней нельзя правильно представить дроби (и вместо ⅓ мы получим 0.33333333333333…, что тоже не совсем правильный результат).
Казалось бы, мелочь: при таких подсчетах разница может быть в одну стотысячную часть (0,00001) или даже меньше.
Но что, если от этого сравнения будет зависеть весь результат работы твоей Очень Серьезной Программы?
if (f1 == f2)
System.out.println("Ракета летит в космос");
else
System.out.println("Запуск отменяется, все расходятся по домам");
Мы явно ожидали, что два числа будут равны, но из-за особенностей внутреннего устройства памяти мы отменили запуск ракеты.
Раз так, нам нужно определиться, как же все-таки сравнить два числа с плавающей точкой, чтобы результат сравнения был более...эммм...предсказуемым.
Итак, правило №1 при сравнении вещественных чисел мы уже усвоили: никогда не используй == при сравнении чисел с плавающей точкой.
Ок, плохих примеров, думаю, достаточно :) Давай рассмотрим хороший пример!
public class Main {
public static void main(String[] args) {
final double threshold = 0.0001;
//прибавляем к нулю 0.1 одиннадцать раз подряд
double f1 = .0;
for (int i = 1; i <= 11; i++) {
f1 += .1;
}
//Умножаем 0.1 на 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 и f2 равны");
else
System.out.println("f1 и f2 не равны");
}
}
Здесь мы по сути делаем то же самое, но меняем способ сравнения чисел.
У нас есть специальное «пороговое» число — 0.0001, одна десятитысячная. Оно может быть и другим. Это зависит от того, насколько точное сравнение тебе нужно в конкретном случае. Можно сделать его и больше, и меньше.
С помощью метода Math.abs() мы получаем модуль числа. Модуль — это значение числа независимо от знака. Например, у чисел -5 и 5 модуль будет одинаковым и равен 5.
Мы вычитаем второе число из первого, и если полученный результат, независимо от знака, будет меньше того порога, который мы установили, значит наши числа равны.
Во всяком случае, они равны до той степени точности, которую мы установили с помощью нашего «порогового числа», то есть как минимум они равны вплоть до одной десятитысячной. Такой способ сравнения избавит тебя от неожиданного поведения, которое мы увидели в случае с ==.
Еще один хороший способ сравнения вещественных чисел — использовать специальный класс BigDecimal. Этот класс специально был создан для хранения очень больших чисел с дробной частью.
В отличие от double и float, при использовании BigDecimal сложение, вычитание и прочие математические операции выполняются не с помощью операторов (+- и т.д.), а с помощью методов.
Вот как это будет выглядеть в нашем случае:
import java.math.BigDecimal;
public class Main {
public static void main(String[] args) {
/*Создаем два объекта BigDecimal - ноль и 0.1.
Делаем то же самое что и раньше - прибавляем 0.1 к нулю 11 раз подряд
В классе BigDecimal сложение осуществляется с помощью метода add()*/
BigDecimal f1 = new BigDecimal(0.0);
BigDecimal pointOne = new BigDecimal(0.1);
for (int i = 1; i <= 11; i++) {
f1 = f1.add(pointOne);
}
/*Здесь тоже ничего не изменилось: создаем два объекта BigDecimal
и умножаем 0.1 на 11
В классе BigDecimal умножение осуществляется с помощью метода multiply()*/
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);
/*Еще одна особенность BigDecimal - объекты чисел нужно сравнивать между
собой с помощью специального метода compareTo()*/
if (f1.compareTo(f2) == 0)
System.out.println("f1 и f2 равны");
else
System.out.println("f1 и f2 не равны");
}
}
Какой же вывод в консоль мы получим?
f1 = 1.1000000000000000610622663543836097232997417449951171875
f2 = 1.1000000000000000610622663543836097232997417449951171875
f1 и f2 равны
Мы получили ровно тот результат, на который рассчитывали. И обрати внимание, насколько точными получились наши числа, и сколько знаков после запятой в них уместилось! Гораздо больше, чем во float и даже в double!
Запомни класс BigDecimal на будущее, он тебе обязательно пригодится :)
Фух! Лекция получилась немаленькая, но ты справился: молодец! :)
Увидимся на следующем занятии, будущий программист!
Java-разработчик в Toshiba Global Commerce Solutions
Александр до IT успел поработать в разных сферах и компаниях: в Guinness World Records, на лондонской Олимпиаде 2021, компании Nie ...
[Читать полную биографию]
Я новичок, изучаю джаву совсем немного, мне лично статья очень помогла соединить в одну картинку кучу разрывочных знаний, стало многое понятно. особенно про двоичную систему, к чему там биты, почему именно такое количество. единственная загадка - почему в конце float в конце всегда f
Если писать код, как в статье, то Intellij Idea выкидывает предупреждение: Unpredictable 'new BigDecimal()' call, которое означает возможную потерю точности. Чтобы это избежать, аргументы, передаваемые в BigDecimal() необходимо передавать строкой (т.е. заключать в кавычки):
BigDecimal pointOne = new BigDecimal("0.1");
В этом случае вывод в консоль будет в ожидаемом виде:
f1 = 1.1
f2 = 1.1
f1 и f2 равны
Очень плохая статья. Непонятно на кого рассчитана.
В водной части статьи размещена ссылка на статью с элементарной информацией об операциях над числами и булевыми значениями. Соответственно, ожидаешь контент примерно того же уровня. Но тут начинаются какие-то скачки с кочки на кочку. Например, сначала автор заостряет внимание на элементарных вещах вроде разницы между типами float и double: "Обрати внимание на столбец «размер в памяти» в таблице выше." (так вот оно что?! а то можно было не заметить), а чуть ниже идут пассажи вроде "У 32-битных чисел с плавающей точкой точность составляет примерно 24 бита, то есть около 7 знаков после запятой." Вот это "то есть 7 знаков после запятой" как вытекает из текста, который был до этого? Начинаю строить предположения, что значущие цифры могут храниться отдельно, тогда в 24 битах можно сохранить 7-мизначное число, но в статье об этом не сказано.
А потом люди в комментариях начинают разгадывать эту большую головоломку, составленную автором в виде статьи - приводят ссылки на стандарты, один другому объясняют, зачем после числа стоит буква "f" и т.д., и т.п. Что само по себе намекает на то, что если в статье есть полезная информация, то подана в неудовлетворительной форме.
«Я гнался за вами три дня, чтобы сказать, что вы мне безразличны».
Я если честно, не против такого подхода, разжёвывать каждую мелочь, но мне кажется, если так делать на постоянной основе, то сама суть текста будет размазываться. Сделайте уже справочник по Джаве. Я бы настаивал на «карте понятий» в виде графа. «Одно вытекает из другого, другое же порождает и третье».
Из личного опыта, могу сказать, что строгий подход (докажи и реализуй сам) оказался самым лучшим. Однако ролевой подход, в смысле подход аналогий, лучше любого упрощения. Что лучше «из двух зол», сказать не могу, но мне кажется оба подхода работали бы в совокупности просто отлично. Типа «плохой и хороший коп»)
я конечно того же мнения, особенно с учетом того, что я так же не понял эти вещи о которых вы щас сказали, но уже как почти 4 года существует чат гпт, который ответит на все самые незначительные вопросы, стоит это того чтобы сразу писать настолько негативный комментарий человеку который по факту вам ничего не должен?
Высказал своё мнение о статье. Человек, который высказывается публично, должен быть готов слушать/читать не только хвалебные отзывы. По-моему это нормально.
По поводу ChatGPT. Я абсолютно не уверен в правильности его ответов. И другим бы рекомендовал перепроверять полученную оттуда информацию. Далеко ходить не надо. Сегодня, в попытках понять в какой последовательности выполняются операции в одном выражении на Java вынужден был обратиться к ChatGPT. Он мне четыре раза давал разные ответы о результате выполнения двух одних и тех же строчек кода. В последней попытке, когда в объяснении порядка операций он мне написал (цитата): "Результат: 3 + 4 = 12" я ответил даже вежливо: "Спасибо. Пока."
Надо отметить, правда, что теперь на тот же вопрос ChatGPT даёт правильный ответ. Возможно даже это он научился отвечая на мои уточняющие вопросы, потому что возражая, я каждый раз аргументировал свою точку зрения. Но сколько ещё таких неправильных ответов получают люди, слепо верящие в технологии?
Тут как и с любым инструментом, с нейросетями надо обращаться осторожно. Вы ведь не ругаетесь на молоток, когда он попадает Вам по пальцу?) И чат-жпт, как по мне, уступает отечественному гига-чат от сбера. Наверно это связано с языковыми особенностями, мне кажется, жпт изначально писался для запросов на английском, но это лишь предположение.
Optimous #3533375, ваше сравнение абсолютно некорректно. Я не использовал инструмент не по его назначению, или небезопасным способом. Просто он ненадёжный. Если хотите, то здесь не я ударил себя молотком по пальцу, а металлическая часть молотка слетела с деревянной ручки.
Т.е. этому "молотку" нельзя доверять, следующий раз он может также развалиться на части и упасть на ногу или отлететь пользователю в лоб.
спасибо за отличную статью, очень круто разъяснили моменты с ошибками при работе с вещественными числами, однозначно лайк, потому что без этой статьи голову можно сломать, когда в консоли выводились бы непонятные числа и с каждым разом вылазили бы более существенные ошибки 👍
единственное, не хватило объяснения про тип float
float f = 0.0f;
что в конце должно быть f и это не опечатка
чтобы указать, что данное значение должно рассматриваться как float, нам надо использовать суффикс f (просто до этой лекции дополнительной про это не упоминается, возможно будет в лекциях дальше)
и что 0.1 можно сокращенно записать как .1
а 0.0 можно сокращенно записать как .0
потому что сначала подумала, что опечатка
Очень полезная статья. Но для новичка обывателя немного удивительная. Жили-жили, а теперь познакомьтесь с классом, где вместо простейших математических операций будут слова. Я даже не представляю какие чУдные открытия несут следующие лекции.
Числа с плавающей точкой бывают с одинарной точностью и двойной.
Числа с одинарной точностью состоят из 32 битов: одного знакового бита, восьми битов для экспоненты и 23 битов для мантиссы.
Числа с двойной точностью — из 64 битов: одного знакового бита, 11 битов для экспоненты и 52 битов для мантиссы.
Все правила их представления записаны в стандарте IEEE 754
Большое спасибо за доходчивое изложение тонких нюансов. Респект!
Мне кажется, что в статье после слов:
"
А у 64-битных чисел (в Java это тип double) — точность примерно 53 бита, то есть примерно 16 знаков после запятой.
Вот пример, который хорошо демонстрирует эту разницу:
"
В примере есть лишний напечатанный символ "f" перед точкой с запятой:
float f = 0.0f;
Вовсе не лишний. Java по умолчанию считает все числа с точкой типом double. Для больших целых чисел типа long используется l после значения: long x = 1234567890l и для float используется f: float y = 12.3456f
В западной матиматической практике для дробей принято использовать точку (point), а в наших регионах запятую. Поэтому, не следует вводить в заблуждение народ, числа не с плавающей точкой, а с плавающей запятой.
Даже в вашем предложении есть эта не состыковка:
"У 32-битных чисел с плавающей точкой (в Java это как раз тип float) точность составляет примерно 24 бита, то есть около 7 знаков после запятой."
А за стрью большое спасибо! :)
Отличная статья, а можете подсказать как высчитывается зависимость почему 24 бита это примерно 7 знаков, а 53 это 16 знаков после запятой, или это фиксированные значения?
Автор на примере 1/3 в десятичной записи показывает, где и как может потеряться точность. Именно этого примера мне не хватало, чтобы понять, почему не все дробные числа можно точно записать в двоичной (и другой) системе.
Спасибо.
Как и с BigInteger - условно бесконечное число. (ограниченное только фреймом кучи (heap) памяти, которую JVM попросит у ОС и получив к ней доступ создаст выделенную область памяти ограниченную представлением JVM (в среднем размер "кучи" в ней 256 Мб, иногда 512 Мб, - и его нужно настраивать внешними средствами вручную). Так вот - работая с BigDecimal / BigInteger - мы всегда будем работать с объектами в куче внутри которых будет происходить динамическое добавление памяти в зависимости от длины числа (в принципе в теории Вы можете потратить весь объем кучи и превысить его емкость "Heap overflow", но это на практике просто немыслимые гипотетические вероятности которые вряд ли кто-то в здравом уме будет проверять.
Главное что нужно знать, это то что работая с объектами класса BigDecimal / BigInteger как и с AtomicInteger вы не будете ограничены стэком (который весит обычно 1-2 Мб = так как вы уже будете работать не с примитивными (фиксированными ) типами данных которые хранятся только в стеке и им же ограничены)
добрый, где можно про это почитать поподробнее?
на собственном опыте вычислил что в линуксе на си стек ограничен 8мб
было бы интересно почитать поподробнее как это происходит в джаве
Я лично не лазил под капотом стека (жалко на это тратить время), чтобы проверить сколько там реально Мб, смотря в какой сборке и какой JDK, смотря на какой платформе, например на Linux ( смотря какая еще его версия ), или на Windows / MacOS . Но читал об этом на StackOverflow, Habr и в документации, что для разных версий и платформ существуют разные реализации JVM и выделенный объем (обычно на каждый поток выделяется 1 Мб, но иногда бывает и больше, до 2 мб = это потолок). Уверен что примерно также дело обстоит и для "Си" на разных платформах.
А что будет, если в примере с BigDecimal использовать тот же объект pointOne для умножения на 11, а не создавать заново объект F2 с таким же значением 0.1?
Насколько я знаю, сравнение с эпсилоном (точностью) тоже не совсем верное, аккуратней с этим.
Достаточно, например, представить вариант когда вы полагаете что сравнение например осуществляется до точности в 0.0001 и используете чужой метод; а разработчик метода например взял и сравнил до 0.001 (и результат будет не тот, что мы ожидаем).
Остаются или классы-обертки (БигДецимал) или сторонние библиотеки типа гуавы.
простыми словами:
Почему имеется погрешность в вычислениях при операциях с вещественными числами?
А всё потому, что компьютер работает с числами в двоичной системе счисления (0 и 1 - есть ток / нету тока) ! и когда мы передаем на обработку числа в десятеричной системе счисления, компьютер преобразует их в двоичную (И ВОТ ПРИ ПЕРЕВОДЕ ИЗ ДЕСЯТЕРИЧНОЙ В ДВОИЧНУЮ СИСТЕМУ СЧИСЛЕНИЯ ИМЕННО ЧИСЕЛ С ПЛАВАЮЩЕЙ ТОЧКОЙ ИМЕЕТСЯ ПОГРЕШНОСТЬ) Например число 0.7 в двоичной системе счисления представиться как 1011001100110011... и 0011 зацикливается до тех пор пока не упрется в конечный бит. (СОВЕТУЮ ЗАГУГЛИТЬ КАК ПЕРЕВОДЯТСЯ ЧИСЛА С ПЛАВАЮЩ.ТОЧКОЙ С 10-й в 2-ю СИСТЕМУ СЧИСЛЕНИЯ). И ВОТ ПОЭТОМУ В ТАКИХ СЛУЧАЯХ ЛУЧШЕ ИСПОЛЬЗОВАТЬ КЛАСС BigDecimal, т.к. у этого класса неограниченный размер памяти в отличии от примитивных классов (int = 32 бита, long = 64 бита, double = 64 бита), и следовательно BigDecimal будет давать более точный результат с наименьшей погрешностью!
Поправьте если что-то не правильно сказал, сам недавно начал изучение
Вопрос только в том, сколько такая неограниченность будет считаться по сравнению с простейшими) а то горе-программист "объявит" на всякий случай все, что можно, а потом и утечки памяти, и скорость просчета в сотни раз ниже...
и даже класс BigDecimal с математической точки зрения решил задачку неверно..... типо (0.1 * 11) = 11
откуда взялись еще 0.0000000000000000610622663543836097232997417449951171875???))) загадка природы.....
Дело в том, что в двоичной системе невозможно точно представить число 0,1. В десятичной системе, кстати, тоже есть подобная проблема: в ней нельзя правильно представить дроби (и вместо ⅓ мы получим 0.33333333333333…, что тоже не совсем правильный результат).
Да, по умолчанию все вещественные (дробные) числf имеют тип double.
Чтобы такое число имело тип float, нужно явно это указывать, ставя f или F в конце числа.
То же самое с целыми числами. По умолчанию, все они int.
я не очень понял этот момент, что значит точность у float 24 бита и 7 цифр, а у double 53 бита и 16 цифр
почему у float именно 7 цифр, хотя в 24 бита вмещается 8 цифр, а в double именно 53 бита, хотя 16 цифр вмещается в 51 бит
Там еще знак обязательно учитывается, из-за особенностей хранения чисел в бинарном виде.
Знак занимает 1 бит.
Но знак нужен и мантиссе, и степени, а значит минимум 2 бита выделить придется.
Мдааа, оказывается кудахтер считает быстро, но не точно. Причём умножает точнее, чем складывает, уже при 0.7+0.1 начинается расхождение (если верить онлайн компилятору). А есть похожие примеры с другими операциями?
Это явление называется накопление ошибки. При умножении одна операция, при сложении на порядок больше - ошибка соответственно получается больше, ее видно наглядно.
Ну а не точно - под капотом, да. На дешевом калькуляторе это тоже видно. Java этот момент просто не скрывает, позволяет обработать по-своему. Другие языки часто скрывают такое, но и контроля тогда меньше.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ