2011-12-14 3 views
2

Известно, что примитивные значения с плавающей запятой java не должны использоваться, когда требуется произвольная точность. Гёц объяснил проблему в своем excellent article.Можно ли избежать опасности с плавающей запятой Java с округлением?

Представьте, что нам нужно добиться произвольной точности в определенном проекте, и у нас нет класса BigDecimal (потому что он недоступен в API, например: JavaME), и не имеет времени для разработки пользовательской реализации. Если мы заранее знаем, что требуется лишь относительно небольшая точность (от 2 до 4 десятичных знаков), можно ли реализовать 100% -ное надежное аварийное обходное решение, использующее поплавковые и двойные типы и функцию округления? И если да, то какую функцию в API можно использовать? В случае, если эта функция недоступна, но все же вы думаете, что она может решить проблему, насколько сложно было бы ее реализовать?

+2

Вы не указали проблему, которую пытаетесь решить, с почти достаточной детализацией. – antlersoft

+1

Предположим, вы хотите точность в 2 цифры. чего вы ожидаете от: 0,01/10 * 10. делать? return 0 или 0.01? если позже - любое конечное число бит не будет достаточным для его достижения. – amit

+0

Хорошо, вопрос обновления. –

ответ

3

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

+0

Но если OP заботится только о двух цифрах, вы можете определить каждое число в диапазоне '[0.095,0.105]' как '0,1'. Очевидно, вы теряете точность при расчете таким образом ... ее нельзя избежать с помощью любого конечного числа бит. но представлять число - это было бы возможно. – amit

+0

False. '0.1' является идеально представимым, если вы вообще не используете арифметику с плавающей запятой (например, мой ответ). –

+1

@YuvalAdam: ОК, но OP говорит: «* реализовать 100% надежное аварийное обходное решение ** с использованием float и double types ** и функцию округления *». Поэтому я не понимаю нисходящее ... Ваше решение довольно хорошо, на самом деле так работает BigDecimal :-). –

0

В этом случае, зачем вообще заниматься арифметикой с плавающей запятой? Просто используйте Integer, умноженный на ваш прецизионный коэффициент.

final int PRECISION = 4; 
Integer yourFloatingValue = Integer.valueOf("467.8142") * Math.pow(10, PRECISION); 

Небольшое значение точности, такие, как 467.8142 будут представлены 4,678,142 и рассчитаны с использованием стандартных Integer операций. Никакой потери точности.

Но, опять же, как @TomaszNurkiewicz упоминается, это именно то, что BigDecimal делает. Поэтому ваш вопрос не имеет никакого смысла. Арифметика с плавающей точкой отлично подходит и может обрабатывать даже те случаи, о которых вы говорили, если программист знает, что она делает.

+3

Как вы собираетесь размножаться/делить, не прибегая к плавающей точке? – DaveRlz

+0

+1 Это, по сути, «fixnum», я оспариваю, что в общем случае нет потери точности, хотя в приведенном примере действительно нет потерь, однако для не-компьютерных ученых Понимаю. – Arafangion

+0

@DaveRlz: Интегральное умножение и деление по-прежнему будут работать. – Arafangion

1

Определить "100% надежный". Значения с плавающей запятой IEEE 754 (которые используются почти на всех языках, это отнюдь не проблема, специфичная для Java) на самом деле делают то, что они предназначены для очень надежной работы. Они просто не всегда ведут себя так, как люди ожидают (десятичные) дробные числа.

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

0

Я думаю, нет, если вы не сможете полностью определить и контролировать все математики до такой степени, чтобы исключить все округление.

Альтернативой может быть, возможно, использование Rationals. Вот один, который я выбил, как эксперимент. Я сомневаюсь, что это оптимально или даже эффективно, но это, безусловно, возможность.

class Rational { 

    private int n; // Numerator. 
    private int d; // Denominator. 

    Rational(int n, int d) { 
    int gcd = gcd(n, d); 
    this.n = n/gcd; 
    this.d = d/gcd; 
    } 

    Rational add(Rational r) { 
    int lcm = lcm(d, r.d); 
    return new Rational((n * lcm)/d + (r.n * lcm)/r.d, lcm); 
    } 

    Rational sub(Rational r) { 
    int lcm = lcm(d, r.d); 
    return new Rational((n * lcm)/d - (r.n * lcm)/r.d, lcm); 
    } 

    Rational mul(Rational r) { 
    return new Rational(n * r.n, d * r.d); 
    } 

    Rational div(Rational r) { 
    return new Rational(n * r.d, d * r.n); 
    } 

    @Override 
    public String toString() { 
    return n + "/" + d; 
    } 

    /** 
    * Returns the least common multiple between two integer values. 
    * 
    * @param a the first integer value. 
    * @param b the second integer value. 
    * @return the least common multiple between a and b. 
    * @throws ArithmeticException if the lcm is too large to store as an int 
    * @since 1.1 
    */ 
    public static int lcm(int a, int b) { 
    return Math.abs(mulAndCheck(a/gcd(a, b), b)); 
    } 

    /** 
    * Multiply two integers, checking for overflow. 
    * 
    * @param x a factor 
    * @param y a factor 
    * @return the product <code>x*y</code> 
    * @throws ArithmeticException if the result can not be represented as an 
    *   int 
    * @since 1.1 
    */ 
    public static int mulAndCheck(int x, int y) { 
    long m = ((long) x) * ((long) y); 
    if (m < Integer.MIN_VALUE || m > Integer.MAX_VALUE) { 
     throw new ArithmeticException("overflow: mul"); 
    } 
    return (int) m; 
    } 

    /** 
    * <p> 
    * Gets the greatest common divisor of the absolute value of two numbers, 
    * using the "binary gcd" method which avoids division and modulo 
    * operations. See Knuth 4.5.2 algorithm B. This algorithm is due to Josef 
    * Stein (1961). 
    * </p> 
    * 
    * @param u a non-zero number 
    * @param v a non-zero number 
    * @return the greatest common divisor, never zero 
    * @since 1.1 
    */ 
    public static int gcd(int u, int v) { 
    if (u * v == 0) { 
     return (Math.abs(u) + Math.abs(v)); 
    } 
    // keep u and v negative, as negative integers range down to 
    // -2^31, while positive numbers can only be as large as 2^31-1 
    // (i.e. we can't necessarily negate a negative number without 
    // overflow) 
     /* assert u!=0 && v!=0; */ 
    if (u > 0) { 
     u = -u; 
    } // make u negative 
    if (v > 0) { 
     v = -v; 
    } // make v negative 
    // B1. [Find power of 2] 
    int k = 0; 
    while ((u & 1) == 0 && (v & 1) == 0 && k < 31) { // while u and v are 
     // both even... 
     u /= 2; 
     v /= 2; 
     k++; // cast out twos. 
    } 
    if (k == 31) { 
     throw new ArithmeticException("overflow: gcd is 2^31"); 
    } 
    // B2. Initialize: u and v have been divided by 2^k and at least 
    // one is odd. 
    int t = ((u & 1) == 1) ? v : -(u/2)/* B3 */; 
    // t negative: u was odd, v may be even (t replaces v) 
    // t positive: u was even, v is odd (t replaces u) 
    do { 
     /* assert u<0 && v<0; */ 
     // B4/B3: cast out twos from t. 
     while ((t & 1) == 0) { // while t is even.. 
     t /= 2; // cast out twos 
     } 
     // B5 [reset max(u,v)] 
     if (t > 0) { 
     u = -t; 
     } else { 
     v = t; 
     } 
     // B6/B3. at this point both u and v should be odd. 
     t = (v - u)/2; 
     // |u| larger: t positive (replace u) 
     // |v| larger: t negative (replace v) 
    } while (t != 0); 
    return -u * (1 << k); // gcd is u*2^k 
    } 

    static void test() { 
    Rational r13 = new Rational(1, 3); 
    Rational r29 = new Rational(2, 9); 
    Rational r39 = new Rational(3, 9); 
    Rational r12 = new Rational(1, 2); 
    Rational r59 = r13.add(r29); 
    Rational r19 = r29.mul(r12); 
    Rational r23 = r39.div(r12); 
    Rational r16 = r12.sub(r13); 
    System.out.println("1/3 = " + r13); 
    System.out.println("2/9 = " + r29); 
    System.out.println("1/3 = " + r39); 
    System.out.println("5/9 = " + r59); 
    System.out.println("1/9 = " + r19); 
    System.out.println("2/3 = " + r23); 
    System.out.println("1/6 = " + r16); 
    } 
} 

Я нашел LCM и НОД код на java2. Вероятно, они могут быть улучшены.

+0

Спасибо за ваш вклад. Однако это не будет работать с иррациональными числами. –

+1

Даже BigDecimal терпит неудачу на иррациональных уровнях. : D – OldCurmudgeon

1

No.

Что половина 0,15, округляется до ближайшей сотой?

В точной арифметике 0,15/2 = 0,075, которая округляет до до 0,08 (при условии соблюдения правил округлой или половинной четности).

В IEEE 754 арифметики, 0,15/2 = 0.07499999999999999722444243843710864894092082977294921875, которая округляет вниз до 0,07.

Смежные вопросы