Можно ли избежать опасности с плавающей точкой в ​​Java с помощью округления? - PullRequest
2 голосов
/ 14 декабря 2011

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

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

Ответы [ 5 ]

3 голосов
/ 14 декабря 2011

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

1 голос
/ 16 декабря 2011

номер

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

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

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

1 голос
/ 14 декабря 2011

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

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

0 голосов
/ 14 декабря 2011

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

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

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 и gcd в java2 . Возможно, их можно улучшить.

0 голосов
/ 14 декабря 2011

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

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

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

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

...