Управление и сравнение с плавающей запятой в java

В Java арифметика с плавающей запятой не представлена ​​точно. Например, этот Java-код:

float a = 1.2; 
float b= 3.0;
float c = a * b; 
if(c == 3.6){
    System.out.println("c is 3.6");
} 
else {
    System.out.println("c is not 3.6");
} 

Печатает «c is not 3.6».

Меня не интересует точность, превышающая 3 десятичных знака (#. ###). Как я могу справиться с этой проблемой, чтобы умножить числа с плавающей запятой и надежно их сравнить?


person Praneeth    schedule 24.05.2010    source источник
comment
Объявите числа с плавающей запятой, такие как: float a = 1.2f;, и удваивает, как double d = 1.2d; Также в вашем if-выражении: if(c == 3.6f)   -  person Martijn Courteaux    schedule 24.05.2010
comment
В дополнение к ответу @bobah я рекомендую посмотреть функцию Math.ulp().   -  person aeracode    schedule 25.03.2012
comment
Используйте BigDecimal для плавающих и двойных манипуляций. См. ссылку.   -  person Aldis    schedule 20.11.2019


Ответы (9)


По общему правилу, числа с плавающей запятой никогда не следует сравнивать как (a == b), а как (Math.abs(a-b) < delta), где дельта - небольшое число.

Значение с плавающей запятой, имеющее фиксированное количество цифр в десятичной форме, не обязательно имеет фиксированное количество цифр в двоичной форме.

Дополнение для ясности:

Хотя строгое == сравнение чисел с плавающей запятой имеет очень мало практического смысла, строгое сравнение < и >, напротив, является допустимым вариантом использования (пример - логический запуск, когда определенное значение превышает пороговое значение: (val > threshold) && panic();)

person bobah    schedule 24.05.2010
comment
Рекомендовать сравнение с использованием допуска - неуместный совет, потому что он уменьшает количество ложных сообщений о неравенстве за счет увеличения числа ложных сообщений о равенстве, и вы не можете знать, приемлемо ли это для приложения, о котором вы ничего не знаете. Приложение может быть «больше заинтересовано» в поиске неравенства, чем в поиске равенства, или может иметь другие спецификации, которым оно должно соответствовать. - person Eric Postpischil; 30.07.2013
comment
@Eric - При работе с числами с плавающей запятой нет понятия идентичности или неравенства, есть только понятие расстояния. Если в формуле, которую я дал в ответе, вы замените < на >, вы получите критерий для сравнения чисел с плавающей запятой на предмет неравенства с точки зрения расстояния. Поразрядное тождество представления чисел с плавающей запятой в памяти компьютера не представляет интереса для большинства практических приложений. - person bobah; 30.07.2013
comment
Речь идет не о битах, представляющих число; это об их ценностях. Арифметика с плавающей запятой действительно имеет равенство. Стандарт IEEE 754 определяет объекты с плавающей запятой для точного представления определенных чисел, а не для представления интервалов. - person Eric Postpischil; 30.07.2013
comment
А пример из реальной жизни есть? - person bobah; 30.07.2013
comment
Пример чего из реальной жизни? - person Eric Postpischil; 30.07.2013
comment
реальный пример, когда вам нужно сравнить числа с плавающей запятой двойной точности с ==, я не знаю ни одного (кроме тестов производителя процессора и т.п.) - person bobah; 30.07.2013
comment
Вы исследуете демпфированный осциллятор и хотите различить недостаточное демпфирование, избыточное демпфирование и критическое демпфирование. Для этого требуется строгий тест, без допусков. Допущение допуска приведет к извлечению квадратного корня из отрицательного числа. Однако, несмотря на этот пример, ваш запрос - обманщик. Совет не сравнивать с допуском не означает сравнения на точное равенство, потому что есть другие варианты. Например, одна из возможностей - вообще отказаться от сравнения; просто сообщите о наилучшем доступном результате, не пытаясь принудительно преобразовать его в квантованный результат. - person Eric Postpischil; 30.07.2013
comment
Независимо от каких-либо примеров, советовать людям сравнивать, используя допуск, возникает фундаментальная проблема. Это увеличивает количество ложных отчетов о равенстве, и, поскольку вы не знаете приложение, вы не можете знать, допустимо ли это или является проблемой. - person Eric Postpischil; 30.07.2013
comment
позвольте нам продолжить это обсуждение в чате - person bobah; 30.07.2013
comment
Точное сравнение с плавающей запятой требует глубокого понимания стандарта IEEE754. Хорошее руководство для этого находится на странице randomascii. wordpress.com/2012/02/25/ - person Ilmo Euro; 01.08.2013
comment
точное сравнение - термин бессмысленный, количественно не поддается. Думаю, я хорошо знаю IEEE754, ответ, который я дал, точно отвечает на вопрос темы, он компактный и однозначный. Ваш комментарий, наоборот, настолько общий, что это почти оффтоп. - person bobah; 01.08.2013

Если вас интересуют числа с фиксированной точностью, вам следует использовать тип с фиксированной точностью, такой как BigDecimal, а не по своей сути приближенный (хотя и с высокой точностью) тип, например float. Существует множество похожих вопросов о переполнении стека, которые более подробно рассматриваются на многих языках.

person David M    schedule 24.05.2010

Я думаю, что это не имеет ничего общего с Java, это происходит с любым числом с плавающей запятой IEEE 754. Это из-за природы представления с плавающей запятой. Любые языки, использующие формат IEEE 754, столкнутся с той же проблемой.

Как было предложено Дэвидом выше, вы должны использовать метод abs класса java.lang.Math, чтобы получить абсолютное значение (отбросьте положительный / отрицательный знак).

Вы можете прочитать это: http://en.wikipedia.org/wiki/IEEE_754_revision, а также Хороший учебник по численным методам решит эту проблему в достаточной степени.

public static void main(String[] args) {
    float a = 1.2f;
    float b = 3.0f;
    float c = a * b;
        final float PRECISION_LEVEL = 0.001f;
    if(Math.abs(c - 3.6f) < PRECISION_LEVEL) {
        System.out.println("c is 3.6");
    } else {
        System.out.println("c is not 3.6");
    }
}
person Daniel Baktiar    schedule 24.05.2010

Я использую этот фрагмент кода в модульных тестах, чтобы сравнить, совпадают ли результаты двух разных вычислений, исключая математические ошибки с плавающей запятой.

Он работает, глядя на двоичное представление числа с плавающей запятой. Большая часть сложности связана с тем, что знак чисел с плавающей запятой не является дополнением до двух. После компенсации это в основном сводится к простому вычитанию, чтобы получить разницу в ULP (объяснено в комментарии ниже).

/**
 * Compare two floating points for equality within a margin of error.
 * 
 * This can be used to compensate for inequality caused by accumulated
 * floating point math errors.
 * 
 * The error margin is specified in ULPs (units of least precision).
 * A one-ULP difference means there are no representable floats in between.
 * E.g. 0f and 1.4e-45f are one ULP apart. So are -6.1340704f and -6.13407f.
 * Depending on the number of calculations involved, typically a margin of
 * 1-5 ULPs should be enough.
 * 
 * @param expected The expected value.
 * @param actual The actual value.
 * @param maxUlps The maximum difference in ULPs.
 * @return Whether they are equal or not.
 */
public static boolean compareFloatEquals(float expected, float actual, int maxUlps) {
    int expectedBits = Float.floatToIntBits(expected) < 0 ? 0x80000000 - Float.floatToIntBits(expected) : Float.floatToIntBits(expected);
    int actualBits = Float.floatToIntBits(actual) < 0 ? 0x80000000 - Float.floatToIntBits(actual) : Float.floatToIntBits(actual);
    int difference = expectedBits > actualBits ? expectedBits - actualBits : actualBits - expectedBits;

    return !Float.isNaN(expected) && !Float.isNaN(actual) && difference <= maxUlps;
}

Вот версия для double точных поплавков:

/**
 * Compare two double precision floats for equality within a margin of error.
 * 
 * @param expected The expected value.
 * @param actual The actual value.
 * @param maxUlps The maximum difference in ULPs.
 * @return Whether they are equal or not.
 * @see Utils#compareFloatEquals(float, float, int)
 */
public static boolean compareDoubleEquals(double expected, double actual, long maxUlps) {
    long expectedBits = Double.doubleToLongBits(expected) < 0 ? 0x8000000000000000L - Double.doubleToLongBits(expected) : Double.doubleToLongBits(expected);
    long actualBits = Double.doubleToLongBits(actual) < 0 ? 0x8000000000000000L - Double.doubleToLongBits(actual) : Double.doubleToLongBits(actual);
    long difference = expectedBits > actualBits ? expectedBits - actualBits : actualBits - expectedBits;

    return !Double.isNaN(expected) && !Double.isNaN(actual) && difference <= maxUlps;
}
person Laurens Holst    schedule 26.07.2013
comment
Вы также можете рассмотреть возможность использования Float.floatToRawIntBits(), проверяя NaN в начале вашего метода. Фактически, floatToIntBits() ничего не делает, кроме проверки результата для NaN, заменяя его заранее определенным целочисленным значением 0x7fc00000. Основная причина для этого заключается в том, что floatToIntBits() на самом деле вызывает floatToRawIntBits(), что замедляет его выполнение. Другой подход - проверить преобразованные биты на 0x7fc00000, но вам не нужны обе проверки. - person Netherwire; 18.07.2018

Это слабость всех представлений с плавающей запятой, и это происходит потому, что некоторые числа, которые имеют фиксированное количество десятичных знаков в десятичной системе, на самом деле имеют бесконечное количество десятичных знаков в двоичной системе. Итак, то, что вы думаете, 1.2 на самом деле похоже на 1.199999999997, потому что при представлении его в двоичном формате он должен отбрасывать десятичные дроби после определенного числа, и вы теряете некоторую точность. Тогда умножение его на 3 фактически дает 3,5999999 ...

http://docs.python.org/py3k/tutorial/floatingpoint.html ‹- это могло бы объяснить это лучше (даже если это для python, это общая проблема представления с плавающей запятой)

person Andrei Fierbinteanu    schedule 24.05.2010
comment
+1 - все системы с плавающей запятой конечной точности страдают от этой проблемы. Независимо от того, какую базу вы выберете, некоторые рациональные числа не могут быть представлены точно. - person Stephen C; 24.05.2010

Как писали другие:

Сравнить числа с плавающей запятой: if (Math.abs(a - b) < delta)

Вы можете написать хороший метод для этого:

public static int compareFloats(float f1, float f2, float delta)
{
    if (Math.abs(f1 - f2) < delta)
    {
         return 0;
    } else
    {
        if (f1 < f2)
        {
            return -1;
        } else {
            return 1;
        }
    }
}

/**
 * Uses <code>0.001f</code> for delta.
 */
public static int compareFloats(float f1, float f2)
{
     return compareFloats(f1, f2, 0.001f);
}

Итак, вы можете использовать это так:

if (compareFloats(a * b, 3.6f) == 0)
{
    System.out.println("They are equal");
}
else
{
    System.out.println("They aren't equal");
}
person Martijn Courteaux    schedule 24.05.2010

Существует класс apache для сравнения двойников: org.apache.commons.math3.util.Precision

Он содержит несколько интересных констант: SAFE_MIN и EPSILON, которые представляют собой максимально возможные отклонения при выполнении арифметических операций.

Он также предоставляет необходимые методы для сравнения, равных или округленных удвоений.

person bvdb    schedule 19.05.2015

Округление - плохая идея. Используйте BigDecimal и при необходимости установите точность. Нравиться:

public static void main(String... args) {
    float a = 1.2f;
    float b = 3.0f;
    float c = a * b;
    BigDecimal a2 = BigDecimal.valueOf(a);
    BigDecimal b2 = BigDecimal.valueOf(b);
    BigDecimal c2 = a2.multiply(b2);
    BigDecimal a3 = a2.setScale(2, RoundingMode.HALF_UP);
    BigDecimal b3 = b2.setScale(2, RoundingMode.HALF_UP);
    BigDecimal c3 = a3.multiply(b3);
    BigDecimal c4 = a3.multiply(b3).setScale(2, RoundingMode.HALF_UP);

    System.out.println(c); // 3.6000001
    System.out.println(c2); // 3.60000014305114740
    System.out.println(c3); // 3.6000
    System.out.println(c == 3.6f); // false
    System.out.println(Float.compare(c, 3.6f) == 0); // false
    System.out.println(c2.compareTo(BigDecimal.valueOf(3.6f)) == 0); // false
    System.out.println(c3.compareTo(BigDecimal.valueOf(3.6f)) == 0); // false
    System.out.println(c3.compareTo(BigDecimal.valueOf(3.6f).setScale(2, RoundingMode.HALF_UP)) == 0); // true
    System.out.println(c3.compareTo(BigDecimal.valueOf(3.6f).setScale(9, RoundingMode.HALF_UP)) == 0); // false
    System.out.println(c4.compareTo(BigDecimal.valueOf(3.6f).setScale(2, RoundingMode.HALF_UP)) == 0); // true
}
person Aldis    schedule 20.11.2019

Чтобы сравнить два числа с плавающей запятой, f1 и f2 с точностью до #.###, я считаю, что вам нужно сделать следующее:

((int) (f1 * 1000 + 0.5)) == ((int) (f2 * 1000 + 0.5))

f1 * 1000 поднимает 3.14159265... до 3141.59265, + 0.5 дает 3142.09265, а (int) отбрасывает десятичные числа, 3142. То есть он включает 3 десятичных знака и правильно округляет последнюю цифру.

person aioobe    schedule 24.05.2010
comment
Лучше сравнить использование эпсилона: подумайте, что произойдет, если f1 == 3.1414999999999 и f2 == 3.1415000000001. - person Mark Dickinson; 25.05.2010
comment
Дерьмо. Я думал, что он у меня был :-) конечно. Я согласен. Сравнивать с помощью эпсилона намного лучше. Но точно ли он сравнивает два числа с плавающей запятой со своими тремя первыми десятичными знаками? - person aioobe; 25.05.2010