$ sudo teach IT
МОДУЛЬ 2 · УРОК 2

Числа с плавающей точкой — float, double, точность и особенности

Изучаем типы float и double, узнаём о научной нотации, проблемах точности, специальных значениях Infinity и NaN, а также о BigDecimal

~25 минут Для новичков Java

Зачем нужны дробные числа?

Представьте, что вы измеряете длину стола. Если стол ровно 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 вопросов

Точность double

Premium