Можно ли избежать опасностей Java с плавающей запятой с округлением?

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

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


person Mister Smith    schedule 14.12.2011    source источник
comment
Вы недостаточно подробно изложили проблему, которую пытаетесь решить.   -  person antlersoft    schedule 14.12.2011
comment
предположим, что вам нужна точность в 2 цифры. что вы ожидаете от: 0.01/10.*10. сделать? вернуть 0 или 0,01? если позднее - любого конечного числа бит будет недостаточно для его достижения.   -  person amit    schedule 14.12.2011
comment
Да, пока это возможно, но я все еще стою в тумане. Не могли бы вы точно указать, что вы хотите сделать и какие операции вам нужны (сложение, вычитание)? Без этой информации никто не сможет сказать вам, насколько это будет сложно.   -  person Thorsten S.    schedule 26.06.2012
comment
Насколько я помню, мне нужно было только сложение (и вычитание).   -  person Mister Smith    schedule 26.06.2012


Ответы (5)


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

person Tomasz Nurkiewicz    schedule 14.12.2011
comment
Но если OP заботятся только о 2 цифрах, вы можете определить каждое число в диапазоне [0.095,0.105) как 0.1. Очевидно, что при таком расчете вы теряете точность... этого нельзя избежать с любым конечным числом битов. но представить число - это возможно. - person amit; 14.12.2011
comment
ЛОЖЬ. 0.1 прекрасно представим, если вы вообще не используете арифметику с плавающей запятой (например, мой ответ). - person Yuval Adam; 14.12.2011
comment
@YuvalAdam: Хорошо, но ОП говорит: реализовать 100% надежный аварийный обходной путь, используя типы float и double и функцию округления. Поэтому я не понимаю отрицательного ответа... Ваше решение довольно хорошее, на самом деле так работает BigDecimal :-). - person Tomasz Nurkiewicz; 14.12.2011

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

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

person Michael Borgwardt    schedule 14.12.2011

No.

Чему равна половина 0,15, округленная до сотых?

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

В арифметике IEEE 754 0,15/2 = 0,07499999999999999722444243843710864894092082977294921875, что округляет в меньшую сторону до 0,07.

person dan04    schedule 16.12.2011

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

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

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

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

person Yuval Adam    schedule 14.12.2011
comment
Как вы собираетесь умножать/делить, не прибегая к плавающей запятой? - person Dave Richardson; 14.12.2011
comment
+1, по сути, это фикснум, я оспариваю, что в общем случае потери точности нет, хотя в приведенном примере действительно нет потери - однако это легче понять неспециалистам в области информатики. - person Arafangion; 14.12.2011
comment
@DaveRlz: целочисленное умножение и деление по-прежнему будут работать. - person Arafangion; 14.12.2011
comment
@DaveRlz — стандартное Integer умножение и деление. Вам действительно нужно доказательство того, почему это работает без использования арифметики с плавающей запятой? - person Yuval Adam; 14.12.2011
comment
@Yuval - я добавил свой комментарий до того, как вы дополнили свой ответ примером. Это был не вызов, это был вопрос. Я не просил доказательств, поэтому, если вы не хотите их предоставлять, пожалуйста, не считайте, что вам это нужно. - person Dave Richardson; 14.12.2011
comment
Это будет реализация собственного класса BigDecimal. 100% надежный, но вы должны быть осторожны при его разработке. Не так просто, как может показаться. - person Mister Smith; 14.12.2011

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

Альтернативой может быть, возможно, использование 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 и gcd на странице java2. Вероятно, их можно улучшить.

person OldCurmudgeon    schedule 14.12.2011
comment
Спасибо за ваш вклад. Однако это не будет работать с иррациональными числами. - person Mister Smith; 14.12.2011
comment
Даже BigDecimal потерпит неудачу с иррациональными числами. :D - person OldCurmudgeon; 14.12.2011