Числа с плавающей точкой — float, double, точность и особенности
Изучаем типы float и double, узнаём о научной нотации, проблемах точности, специальных значениях Infinity и NaN, а также о BigDecimal
Зачем нужны дробные числа?
Представьте, что вы измеряете длину стола. Если стол ровно 1 метр — целое число 1 подходит идеально. Но если стол 1.75 метра? Или 0.333... метра (треть метра)? Целочисленные типы (byte, short, int, long) не могут хранить дробные значения — они работают только с целыми числами.
Для дробных чисел Java предоставляет два типа: float и double. Название «плавающая точка» (floating point) связано с тем, что десятичная точка может «плавать» — перемещаться влево или вправо в зависимости от величины числа.
Это как измерение длины: можно сказать «1 метр 75 сантиметров» (точка после целых метров), а можно «0.00175 километра» (точка после километров). Точка «плавает», но число одно и то же.
Тип float — одинарная точность
Тип float (от слова floating point — плавающая точка) занимает 32 бита (4 байта) памяти. Он хранит числа с одинарной точностью по стандарту IEEE 754.
Основные характеристики float:
- Примерно 7 значащих цифр точности
- Диапазон примерно от ±1.4 × 10⁻⁴⁵ до ±3.4 × 10³⁸
- Для литерала float обязателен суффикс F (или f)
public class FloatExample {
public static void main(String[] args) {
float price = 9.99F;
float temperature = -36.6F;
float pi = 3.14159F;
float gravity = 9.8F;
float distance = 150_000_000F;
System.out.println("Цена: " + price);
System.out.println("Температура: " + temperature);
System.out.println("Число Пи: " + pi);
System.out.println("Гравитация: " + gravity);
System.out.println("Расстояние до Солнца (км): " + distance);
}
}
Важно: Без суффикса F компилятор считает дробное число типом double, и присваивание в float вызовет ошибку. Запись float x = 3.14; — ошибка компиляции! Нужно float x = 3.14F;.
Почему 7 значащих цифр? Потому что 32 бита — это ограничение. Из них 24 бита отводятся на мантиссу (значащую часть числа), 8 бит — на показатель степени (порядок), и 1 бит — на знак. 2²⁴ = 16 777 216, что дает примерно 7 десятичных знаков точности.
Тип double — двойная точность
Тип double (от слова double precision — двойная точность) занимает 64 бита (8 байт) памяти. Это тип по умолчанию для всех дробных литералов в Java.
Основные характеристики double:
- Примерно 15-17 значащих цифр точности
- Диапазон примерно от ±4.9 × 10⁻³²⁴ до ±1.7 × 10³⁰⁸
- Суффикс D (или d) опционален
public class DoubleExample {
public static void main(String[] args) {
double pi = 3.141592653589793;
double e = 2.718281828459045;
double speedOfLight = 299_792_458.0;
double avogadro = 6.02214076e23;
System.out.println("Число Пи: " + pi);
console.log("Число Эйлера: " + e);
System.out.println("Скорость света (м/с): " + speedOfLight);
System.out.println("Число Авогadro: " + avogadro);
}
}
Суффикс D опционален — компилятор автоматически считает все дробные литералы без суффикса типом double. Записи 3.14 и 3.14D абсолютно эквивалентны.
Double в 2 раза больше float и примерно в 2 раза точнее. Для большинства практических задач double предпочтительнее float, потому что его точности хватает для подавляющего большинства вычислений.
Научная нотация — запись очень больших и малых чисел
Научная нотация (или экспоненциальная запись) позволяет записать очень большие или очень маленькие числа компактно. В Java она записывается через символ e (или E), за которым следует показатель степени:
public class ScientificNotation {
public static void main(String[] args) {
double lightYear = 9.461e15;
double atomSize = 1.0e-10;
double electronMass = 9.109e-31;
double sunMass = 1.989e30;
System.out.println("Световой год (м): " + lightYear);
System.out.println("Размер атома (м): " + atomSize);
System.out.println("Масса электрона (кг): " + electronMass);
System.out.println("Масса Солнца (кг): " + sunMass);
}
}
Запись 9.461e15 означает 9.461 × 10¹⁵ = 9 461 000 000 000 000. Положительный показатель — число больше 1, отрицательный — меньше 1.
Научная нотация особенно полезна, когда нужно выразить масштаб: размер атома (~0.0000000001 м) гораздо удобнее писать как 1.0e-10.
Проблема точности — самая главная ловушка!
Это один из самых важных разделов этого урока. Типы float и double хранят числа в двоичном представлении, и некоторые десятичные дроби не могут быть точно представлены в двоичной системе.
Представьте, что вы пытаетесь представить число 1/3 в десятичной системе: 0.3333333... Оно никогда не закончится! Точно так же некоторые десятичные дроби не имеют конечного представления в двоичной системе.
public class PrecisionProblem {
public static void main(String[] args) {
double a = 0.1;
double b = 0.2;
double sum = a + b;
System.out.println("0.1 + 0.2 = " + sum);
System.out.println("Равно ли 0.3? " + (sum == 0.3));
float f1 = 0.1F;
float f2 = 0.2F;
float fsum = f1 + f2;
System.out.println("float: 0.1 + 0.2 = " + fsum);
}
}
Вы увидите, что 0.1 + 0.2 не равно ровно 0.3! Результат будет примерно 0.30000000000000004. Это не баг — это фундаментальное свойство представления дробных чисел в двоичной системе.
Никогда не сравнивайте double/float через == ! Из-за погрешности вычислений два математически равных числа могут иметь различные представления в памяти. Вместо этого сравнивайте разницу с допустимой погрешностью (epsilon).
public class SafeComparison {
public static void main(String[] args) {
double a = 0.1 + 0.2;
double b = 0.3;
double epsilon = 1e-10;
boolean isEqual = Math.abs(a - b) < epsilon;
System.out.println("Безопасное сравнение: " + isEqual);
System.out.println("Небезопасное сравнение: " + (a == b));
}
}
Сравнение float и double
| Характеристика | float | double |
|---|---|---|
| Размер | 4 байта (32 бита) | 8 байт (64 бита) |
| Точность | ~7 значащих цифр | ~15-17 значащих цифр |
| Суффикс литерала | F (обязателен) | D (опционален) |
| По умолчанию | Нет | Да |
| Минимальное | ~1.4e-45 | ~4.9e-324 |
| Максимальное | ~3.4e38 | ~1.7e308 |
Специальные значения: Infinity и NaN
При определённых арифметических операциях с double и float могут возникнуть специальные значения:
Положительная бесконечность (+Infinity)
Возникает при делении положительного числа на 0 или при превышении максимального значения:
double positiveInfinity = 1.0 / 0.0;
System.out.println("1.0 / 0.0 = " + positiveInfinity);
System.out.println("Это бесконечность? " + Double.isInfinite(positiveInfinity));
Отрицательная бесконечность (-Infinity)
Возникает при делении отрицательного числа на 0:
double negativeInfinity = -1.0 / 0.0;
System.out.println("-1.0 / 0.0 = " + negativeInfinity);
NaN (Not a Number)
Возникает при неопределённых операциях: 0/0, бесконечность минус бесконечность, корень из отрицательного числа:
double nan1 = 0.0 / 0.0;
double nan2 = Math.sqrt(-1);
double nan3 = Double.POSITIVE_INFINITY - Double.POSITIVE_INFINITY;
System.out.println("0.0 / 0.0 = " + nan1);
System.out.println("sqrt(-1) = " + nan2);
System.out.println("Infinity - Infinity = " + nan3);
System.out.println("NaN == NaN? " + (Double.NaN == Double.NaN));
System.out.println("Проверка через isNaN: " + Double.isNaN(nan1));
Особенность NaN: NaN не равен самому себе! Выражение Double.NaN == Double.NaN возвращает false. Это стандарт IEEE 754. Для проверки используйте метод Double.isNaN(value).
Константы Double и Float
Классы-обёртки Double и Float содержат полезные константы и методы:
public class DoubleConstants {
public static void main(String[] args) {
System.out.println("MAX_VALUE: " + Double.MAX_VALUE);
System.out.println("MIN_VALUE: " + Double.MIN_VALUE);
System.out.println("POSITIVE_INFINITY: " + Double.POSITIVE_INFINITY);
System.out.println("NEGATIVE_INFINITY: " + Double.NEGATIVE_INFINITY);
System.out.println("NaN: " + Double.NaN);
System.out.println("Epsilon: " + Double.MIN_VALUE);
System.out.println("True epsilon: " + Math.ulp(1.0));
}
}
Обратите внимание: Double.MIN_VALUE — это самое малое положительное число (не самое малое по модулю!), равное примерно 4.9e-324. Для получения минимального отрицательного числа используйте -Double.MAX_VALUE.
BigDecimal — точные вычисления
Для situations, где точность критична (финансовые расчёты, бухгалтерия), Java предоставляет класс BigDecimal. Он хранит число в десятичном представлении и не страдает от погрешности double.
import java.math.BigDecimal;
public class BigDecimalExample {
public static void main(String[] args) {
BigDecimal a = new BigDecimal("0.1");
BigDecimal b = new BigDecimal("0.2");
BigDecimal sum = a.add(b);
BigDecimal expected = new BigDecimal("0.3");
System.out.println("BigDecimal: 0.1 + 0.2 = " + sum);
System.out.println("Равно ли 0.3? " + sum.equals(expected));
System.out.println();
System.out.println("double: 0.1 + 0.2 = " + (0.1 + 0.2));
System.out.println("Равно ли 0.3? " + ((0.1 + 0.2) == 0.3));
BigDecimal price1 = new BigDecimal("19.99");
BigDecimal price2 = new BigDecimal("29.99");
BigDecimal total = price1.add(price2);
System.out.println();
System.out.println("Итого: " + total);
}
}
Правило: Для денег и финансовых расчётов ВСЕГДА используйте BigDecimal. Для научных вычислений и графики double обычно достаточен.
BigDecimal создаётся из строки, а не из double (чтобы избежать погрешности на этапе создания):
import java.math.BigDecimal;
public class BigDecimalCreation {
public static void main(String[] args) {
BigDecimal good = new BigDecimal("0.1");
BigDecimal bad = new BigDecimal(0.1);
System.out.println("Из строки: " + good);
System.out.println("Из double: " + bad);
BigDecimal precise = BigDecimal.valueOf(0.1);
System.out.println("valueOf: " + precise);
}
}
Как видите, new BigDecimal(0.1) сохраняет погрешность double, а new BigDecimal("0.1") — точное значение. Используйте строковый конструктор или метод BigDecimal.valueOf().
Неявное и явное преобразование
При преобразовании между числовыми типами действуют правила расширения и сужения:
public class TypeConversion {
public static void main(String[] args) {
int i = 100;
double d = i;
System.out.println("int → double: " + d);
double pi = 3.14159;
int truncated = (int) pi;
System.out.println("double → int (сужение): " + truncated);
float f = 3.14F;
double promoted = f;
System.out.println("float → double: " + promoted);
long big = 1000000L;
float fromLong = big;
System.out.println("long → float: " + fromLong);
}
}
Внимание: При преобразовании long → float может произойти потеря точности, хотя это не является ошибкой компиляции. Long имеет 64 бита, float — только 32. Некоторые значения long не могут быть точно представлены в float.
Практический пример: конвертер температуры
Давайте создадим программу для конвертации температуры между Цельсием, Фаренгейтом и Кельвином:
public class TemperatureConverter {
public static void main(String[] args) {
double celsius = 25.0;
double fahrenheit = celsius * 9.0 / 5.0 + 32.0;
double kelvin = celsius + 273.15;
System.out.println("=== Конвертер температуры ===");
System.out.printf("Цельсий: %.1f°C%n", celsius);
System.out.printf("Фаренгейт: %.1f°F%n", fahrenheit);
System.out.printf("Кельвин: %.1f K%n", kelvin);
System.out.println();
System.out.println("--- Таблица ---");
for (double c = -40; c <= 100; c += 20) {
double f = c * 9.0 / 5.0 + 32.0;
double k = c + 273.15;
System.out.printf("%6.1f°C = %7.1f°F = %7.1f K%n", c, f, k);
}
}
}
Здесь мы используем printf для форматированного вывода: %.1f означает дробное число с 1 знаком после запятой, %n — перевод строки.
Методы класса Math для работы с дробными числами
Класс Math предоставляет множество полезных методов:
public class MathMethods {
public static void main(String[] args) {
double x = 2.7;
System.out.println("abs(-5.5) = " + Math.abs(-5.5));
System.out.println("ceil(2.3) = " + Math.ceil(2.3));
System.out.println("floor(2.9) = " + Math.floor(2.9));
System.out.println("round(2.5) = " + Math.round(2.5));
System.out.println("max(3.14, 2.72) = " + Math.max(3.14, 2.72));
System.out.println("min(3.14, 2.72) = " + Math.min(3.14, 2.72));
System.out.println();
System.out.println("sqrt(144) = " + Math.sqrt(144));
System.out.println("pow(2, 10) = " + Math.pow(2, 10));
System.out.println("PI = " + Math.PI);
System.out.println("E = " + Math.E);
System.out.println();
System.out.println("log(2.718) = " + Math.log(Math.E));
System.out.println("sin(PI/2) = " + Math.sin(Math.PI / 2));
}
}
Когда использовать float, а когда double?
Практические рекомендации:
- double — используйте по умолчанию. Достаточен для научных вычислений, графики, физики
- float — экономит память, используется в 3D-графике (OpenGL), играх, обработке изображений
- BigDecimal — для финансов, бухгалтерии, любых расчётов, где копейки важны
В современных компьютерах разница в производительности между float и double минимальна. Выбирайте тип исходя из требуаний к точности, а не к производительности.
Частые ошибки новичков
Ошибка 1: Забытый суффикс F
float x = 3.14; // ОШИБКА! 3.14 — это double
float y = 3.14F; // Правильно
Ошибка 2: Сравнение через ==
double a = 0.1 + 0.2;
double b = 0.3;
if (a == b) { ... } // НЕНАДЁЖНО!
if (Math.abs(a - b) < 1e-10) { ... } // ПРАВИЛЬНО
Ошибка 3: Деление double на 0
double result = 10.0 / 0.0;
System.out.println(result); // Infinity (не ошибка!)
int result2 = 10 / 0; // Аритмическое исключение!
Ошибка 4: Использование double для денег
double price = 0.1;
double total = 0.0;
for (int i = 0; i < 10; i++) {
total += price;
}
System.out.println(total); // 0.9999999999999999, а не 1.0!
Полный пример: калькулятор площади и объёма
public class GeometryCalculator {
public static void main(String[] args) {
double radius = 5.0;
double height = 10.0;
double circleArea = Math.PI * radius * radius;
double cylinderVolume = circleArea * height;
System.out.println("=== Геометрический калькулятор ===");
System.out.printf("Радиус: %.1f%n", radius);
System.out.printf("Высота: %.1f%n", height);
System.out.printf("Площадь круга: %.4f%n", circleArea);
System.out.printf("Объём цилиндра: %.4f%n", cylinderVolume);
System.out.println();
System.out.println("=== Квадратный корень ===");
for (int i = 1; i <= 10; i++) {
double sqrt = Math.sqrt(i);
System.out.printf("sqrt(%d) = %.6f%n", i, sqrt);
}
System.out.println();
System.out.println("=== Степени двойки ===");
for (int i = 0; i <= 10; i++) {
double power = Math.pow(2, i);
System.out.printf("2^%d = %.0f%n", i, power);
}
}
}
Итоги урока
- float — 4 байта, ~7 знаков точности, суффикс F обязателен
- double — 8 байт, ~15-17 знаков точности, суффикс D опционален
- По умолчанию дробные литералы — это double
- Никогда не сравнивайте float/double через == из-за погрешности
- Научная нотация: 1.0e10 = 10 000 000 000
- Деление на 0.0 даёт Infinity, не ошибку
- 0.0 / 0.0 и sqrt(-1) дают NaN
- Для денег используйте BigDecimal
- Для научных вычислений используйте double
- Для 3D-графики и игр можно использовать float
Тест: Числа с плавающей точкой — float, double
10 вопросов