Привет!
В сегодняшней лекции расскажем о числах в 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
на будущее, он тебе обязательно пригодится :)
Фух! Лекция получилась немаленькая, но ты справился: молодец! :)
Увидимся на следующем занятии, будущий программист!
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ