Сохраняйте точность с двойным в Java

149
public class doublePrecision {
    public static void main(String[] args) {

        double total = 0;
        total += 5.6;
        total += 5.8;
        System.out.println(total);
    }
}

Приведенный выше код печатает:

11.399999999999

Как мне заставить это просто напечатать (или быть в состоянии использовать это как) 11.4?

Deinumite
источник
1
Связанный: математика с плавающей запятой сломана?
Бернхард Баркер

Ответы:

151

Как уже упоминали другие, вы, вероятно, захотите использовать BigDecimalкласс, если вы хотите иметь точное представление 11.4.

Теперь небольшое объяснение, почему это происходит:

Эти floatи doubleпримитивные типы в Java являются плавающей точкой число, где число хранится в виде двоичного представления фракции и экспоненты.

Более конкретно, значение с плавающей запятой двойной точности, такое как doubleтип, является 64-битным значением, где:

  • 1 бит обозначает знак (положительный или отрицательный).
  • 11 бит для показателя степени.
  • 52 бита для значащих цифр (дробная часть в двоичном виде).

Эти части объединяются для создания doubleпредставления значения.

(Источник: Википедия: Двойная точность )

Подробное описание того, как обрабатываются значения с плавающей запятой в Java, см. В разделе 4.2.3. Типы, форматы и значения с плавающей запятой. Спецификации языка Java.

В byte, char, int, longтипов с фиксированной точкой число, которые являются точными представлениями чисел. В отличие от чисел с фиксированной запятой, числа с плавающей запятой в некоторых случаях (можно предположить «большую часть времени») не смогут вернуть точное представление числа. Это причина, почему вы в конечном итоге 11.399999999999в результате 5.6 + 5.8.

Когда требуется точное значение, например, 1,5 или 150,1005, вы захотите использовать один из типов с фиксированной запятой, который сможет точно представлять число.

Как уже упоминалось несколько раз, в Java есть BigDecimalкласс, который будет обрабатывать очень большие числа и очень маленькие числа.

Из Java API Reference для BigDecimalкласса:

Неизменные знаковые десятичные числа произвольной точности. BigDecimal состоит из целочисленного немасштабированного значения произвольной точности и 32-битной целочисленной шкалы. Если ноль или положительный, шкала представляет собой количество цифр справа от десятичной точки. Если значение отрицательное, немасштабированное значение числа умножается на десять до степени отрицания шкалы. Следовательно, значение числа, представленного BigDecimal, равно (unscaledValue × 10 ^ -scale).

Было много вопросов о переполнении стека, касающихся вопроса о числах с плавающей запятой и его точности. Вот список связанных вопросов, которые могут представлять интерес:

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

coobird
источник
3
На самом деле обычно существует 53 значащих бита, потому что 1 перед «десятичной» точкой подразумевается для всех значений, кроме денормализованных, что дает дополнительный бит точности. например, 3 сохраняется как (1.) 1000 ... x 2 ^ 1, в то время как 0.5 сохраняется как (1.) 0000 ... x 2 ^ -1 Когда значение денормализовано (все биты экспоненты равны нулю), можно, и обычно будет меньше значащих цифр, например, 1 x 2 ^ -1030 хранится как (0.) 00000001 x 2 ^ -1022, поэтому семь значащих цифр были принесены в жертву в масштабе.
Сара Филлипс
1
Следует отметить, что хотя BigDecimalэто намного медленнее, чем doubleв этом случае, это не нужно, поскольку double имеет 15 десятичных знаков точности, вам просто нужно округлить.
Питер Лоури,
2
@PeterLawrey Имеет 15 десятичных знаков точности, если они все до десятичной точки. Все может произойти после десятичной запятой из-за несоизмеримости десятичной и двоичной дробей.
маркиз Лорн
@EJP Вы правы, он имеет около 15 значащих цифр точности. Это может быть 16, но безопаснее предположить, что это 15 или, возможно, 14.
Питер Лоури
Исправление @PeterLawrey EJP было связано с моим вопросом: stackoverflow.com/questions/36344758/… не могли бы вы рассказать, почему это не совсем 15 и в каких ситуациях это может быть 16 или 14?
Шивам Синха
103

Например, когда вы вводите двойное число, 33.33333333333333полученное значение фактически является ближайшим представимым значением двойной точности, а именно:

33.3333333333333285963817615993320941925048828125

Разделив это на 100, получим:

0.333333333333333285963817615993320941925048828125

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

0.3333333333333332593184650249895639717578887939453125

Когда вы выводите это значение, оно снова округляется до 17 десятичных цифр, давая:

0.33333333333333326
Стивен Кэнон
источник
114
Для любого, кто читает это в будущем и озадачен, почему ответ не имеет ничего общего с вопросом: какой-то модератор решил объединить вопрос, на который я (и другие) ответили, с этим, довольно другим, вопросом.
Стивен Кэнон
Как узнать точное двойное значение?
Майкл Яворский
@mikeyaworski en.wikipedia.org/wiki/Double-precision_floating-point_format См. примеры двойной точности
Jaydee
23

Если вы просто хотите обрабатывать значения как дроби, вы можете создать класс Fraction, который содержит поле числителя и знаменателя.

Написать методы для сложения, вычитания, умножения и деления, а также метод toDouble. Таким образом, вы можете избежать поплавков во время расчетов.

РЕДАКТИРОВАТЬ: Быстрое внедрение,

public class Fraction {

private int numerator;
private int denominator;

public Fraction(int n, int d){
    numerator = n;
    denominator = d;
}

public double toDouble(){
    return ((double)numerator)/((double)denominator);
}


public static Fraction add(Fraction a, Fraction b){
    if(a.denominator != b.denominator){
        double aTop = b.denominator * a.numerator;
        double bTop = a.denominator * b.numerator;
        return new Fraction(aTop + bTop, a.denominator * b.denominator);
    }
    else{
        return new Fraction(a.numerator + b.numerator, a.denominator);
    }
}

public static Fraction divide(Fraction a, Fraction b){
    return new Fraction(a.numerator * b.denominator, a.denominator * b.numerator);
}

public static Fraction multiply(Fraction a, Fraction b){
    return new Fraction(a.numerator * b.numerator, a.denominator * b.denominator);
}

public static Fraction subtract(Fraction a, Fraction b){
    if(a.denominator != b.denominator){
        double aTop = b.denominator * a.numerator;
        double bTop = a.denominator * b.numerator;
        return new Fraction(aTop-bTop, a.denominator*b.denominator);
    }
    else{
        return new Fraction(a.numerator - b.numerator, a.denominator);
    }
}

}
Вирусный Шах
источник
1
Неужели так numeratorи denominatorдолжно быть int? Зачем вам нужна точность с плавающей точкой?
Самир Талвар
Думаю, в этом нет необходимости, но он избегает приведения в функцию toDouble, поэтому код читается лучше.
Вирусный Шах
5
ViralShah: Он также потенциально вводит ошибку с плавающей точкой при работе с математическими операциями. Учитывая, что цель этого упражнения состоит в том, чтобы избежать именно этого, кажется разумным изменить его.
Самир Талвар
Отредактировано для использования целых вместо двойных по причинам, упомянутым Самиром Талваром выше.
Вирусный Шах
3
Эта реализация дробей имеет проблемы, поскольку она не сводит их к простейшей форме. 2/3 * 1/2 дают 2/6, где вы действительно хотите, чтобы ответ был 1/3. В идеале в конструкторе вы хотите найти gcd числителя и делителя и разделить их на это.
Salix alba
15

Заметьте, что у вас возникнет та же проблема, если вы используете десятичную арифметику с ограниченной точностью и хотите иметь дело с 1/3: 0,333333333 * 3 - это 0,9999999999, а не 1,00000000.

К сожалению, 5.6, 5.8 и 11.4 просто не являются круглыми числами в двоичном формате, потому что они включают пятые. Таким образом, их представление с плавающей точкой не является точным, так же как 0,3333 не совсем 1/3.

Если все числа, которые вы используете, являются неповторяющимися десятичными числами, и вы хотите получить точные результаты, используйте BigDecimal. Или, как говорили другие, если ваши значения похожи на деньги в том смысле, что все они кратны 0,01, или 0,001, или что-то еще, умножьте все на фиксированную степень 10 и используйте int или long (сложение и вычитание тривиально: следите за умножением).

Тем не менее, если вы довольны двоичными данными для расчета, но хотите просто распечатать их в более дружественном формате, попробуйте java.util.FormatterилиString.format . В строке формата укажите точность, меньшую полной точности в два раза. Например, для 10 значащих цифр 11,399999999999 равно 11,4, поэтому результат будет почти таким же точным и более понятным для человека в тех случаях, когда двоичный результат очень близок к значению, требующему только несколько десятичных знаков.

Точность задания зависит немного от того, сколько математики вы выполнили со своими числами - в общем, чем больше вы делаете, тем больше ошибок накапливается, но некоторые алгоритмы накапливают его намного быстрее, чем другие (их называют «нестабильными», так как в отличие от "стабильных" в отношении ошибок округления). Если все, что вы делаете - это добавляете несколько значений, то я предполагаю, что отбрасывание только одного десятичного знака точности решит проблему. Эксперимент.

Стив Джессоп
источник
3
Нет, не используйте double с денежными значениями! Вам нужна точность с деньгами, используйте вместо этого BigDecimal. В противном случае ваш ответ хороший. Если вам нужна точность, используйте BigDecimal, если точность не так важна, вы можете использовать float или double.
MetroidFan2002
1
Вопрос больше не утверждает или подразумевает, что деньги вовлечены. Я специально говорю, чтобы использовать BigDecimal или целые числа для денег. В чем проблема?
Стив Джессоп
1
И равно «не использовать double для денег» - «не использовать BigDecimal или double для третей». Но иногда проблема связана с делением, и в этом случае все базисы, не делимые на все основные факторы всех знаменателей, примерно одинаково плохи.
Стив Джессоп
1
.9999 = 1, если ваша точность меньше 4 значащих цифр
Брайан Лихи
9

Возможно, вы захотите изучить использование java-класса java.math.BigDecimal, если вам действительно нужна точность математики. Вот хорошая статья от Oracle / Sun по делу BigDecimal . Хотя вы никогда не можете представить 1/3, как кто-то упоминал, вы можете иметь право решать , как именно вы хотите точный результат будет. setScale () твой друг .. :)

Хорошо, потому что у меня сейчас слишком много времени, вот пример кода, который относится к вашему вопросу:

import java.math.BigDecimal;
/**
 * Created by a wonderful programmer known as:
 * Vincent Stoessel
 * xaymaca@gmail.com
 * on Mar 17, 2010 at  11:05:16 PM
 */
public class BigUp {

    public static void main(String[] args) {
        BigDecimal first, second, result ;
        first = new BigDecimal("33.33333333333333")  ;
        second = new BigDecimal("100") ;
        result = first.divide(second);
        System.out.println("result is " + result);
       //will print : result is 0.3333333333333333


    }
}

и чтобы подключить мой новый любимый язык, Groovy, вот еще один пример того же:

import java.math.BigDecimal

def  first =   new BigDecimal("33.33333333333333")
def second = new BigDecimal("100")


println "result is " + first/second   // will print: result is 0.33333333333333
Vinny
источник
5

Уверен, вы могли бы превратить это в пример из трех строк. :)

Если вам нужна точная точность, используйте BigDecimal. В противном случае вы можете использовать целые числа, умноженные на 10 ^, с любой точностью, которую вы хотите.

Dustin
источник
5

Как отметили другие, не все десятичные значения могут быть представлены в двоичном виде, поскольку десятичное число основано на степенях 10, а двоичное - на степенях двух.

Если точность имеет значение, используйте BigDecimal, но если вы просто хотите дружественный вывод:

System.out.printf("%.2f\n", total);

Дам тебе:

11.40
Draemon
источник
5

Вы не можете, потому что 7.3 не имеет конечного представления в двоичном формате. Ближайшее, что вы можете получить, это 2054767329987789/2 ** 48 = 7,3 + 1/1407374883553280.

Взгляните на http://docs.python.org/tutorial/floatingpoint.html для дальнейшего объяснения. (Он находится на сайте Python, но у Java и C ++ одна и та же «проблема».)

Решение зависит от того, что именно является вашей проблемой:

  • Если вам просто не нравится видеть все эти цифры шума, исправьте форматирование строки. Не отображайте более 15 значащих цифр (или 7 для числа с плавающей запятой).
  • Если неточность ваших чисел нарушает такие вещи, как операторы «если», то вы должны написать if (abs (x - 7.3) <TOLERANCE) вместо if (x == 7.3).
  • Если вы работаете с деньгами, то, что вы, вероятно, действительно хотите, это десятичная фиксированная точка. Храните целое число центов или любую меньшую единицу вашей валюты.
  • (ОЧЕНЬ НЕВЕРНО) Если вам нужно более 53 значащих бит (15-16 значащих цифр) точности, используйте высокоточный тип с плавающей точкой, такой как BigDecimal.
dan04
источник
7.3 может не иметь конечного представления в двоичном формате, но я уверен, что получаю -7.3, когда пробую то же самое в C ++
неправильное имя пользователя
2
неправильное имя: нет. Это просто показывает это. Используйте формат «% .17g» (или еще лучше, «% .51g»), чтобы увидеть реальный ответ.
dan04
4
private void getRound() {
    // this is very simple and interesting 
    double a = 5, b = 3, c;
    c = a / b;
    System.out.println(" round  val is " + c);

    //  round  val is  :  1.6666666666666667
    // if you want to only two precision point with double we 
            //  can use formate option in String 
           // which takes 2 parameters one is formte specifier which 
           // shows dicimal places another double value 
    String s = String.format("%.2f", c);
    double val = Double.parseDouble(s);
    System.out.println(" val is :" + val);
    // now out put will be : val is :1.67
}
sravan
источник
3

Используйте java.math.BigDecimal

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

Кевин Кроуэлл
источник
1
-1 за слепую рекомендацию BigDecimal. Если вам на самом деле не нужна десятичная арифметика (т. Е. Если вы делаете расчеты с деньгами), то BigDecimal вам не поможет. Это не решает все ваши ошибки с плавающей запятой: вам все еще приходится иметь дело с 1/3 * 3 = 0.9999999999999999999999999999 и sqrt (2) ** 2 = 1.999999999999999999999999999. Кроме того, BigDecimal несет в себе огромный штраф скорости. Хуже того, из-за отсутствия перегрузки операторов в Java вы должны переписать весь свой код.
dan04
2
@ dan04 - Если вы делаете расчеты с деньгами, зачем использовать плавающее представление, зная присущую ему ошибку .... Поскольку доли центов нет, вы можете использовать десятичную и вычислять центы вместо приблизительного доллара, у вас есть точная сумма центов. Если вы действительно хотите получить долю цента, используйте aa long и считайте тысячи центов. Более того, ОП не упомянул иррациональные числа, все, что его беспокоило, это сложение. Внимательно прочитайте пост и поймите проблему, прежде чем ответить, это может избавить вас от смущения.
Newtopian
3
@Newtopian: мне нечего смущаться. ФП НЕ упоминал ни о деньгах, ни о каких-либо признаках того, что его проблеме свойственна десятичная дробь.
dan04
@ dan04 - Нет, ОП не сделал ... ВЫ сделали это, и Blindly предложили неконтролируемое мнение о том, что, скорее всего, было вполне приемлемым ответом, учитывая недостаточное количество предоставленных деталей
Newtopian
2

Умножьте все на 100 и сохраните в длинных центах.

Пол Томблин
источник
2
@Draemon - посмотрите на пост перед последним редактированием - все эти вещи "shoppingTotal", "calcGST" и "calcPST" мне кажутся деньгами.
Пол Томблин
2

Компьютеры хранят числа в двоичном виде и не могут точно представлять числа, такие как 33.333333333 или 100.0. Это одна из хитрых вещей с использованием двойников. Вам придется просто округлить ответ, прежде чем показывать его пользователю. К счастью, в большинстве приложений вам не нужно столько десятичных знаков.

Джей Аскрен
источник
Я делаю некоторые расчеты шансов, я предпочел бы иметь максимально возможную точность. Но я понимаю , что есть ограничения
Aly
2

Числа с плавающей запятой отличаются от действительных чисел тем, что для любого заданного числа с плавающей запятой существует следующий более высокий номер с плавающей запятой. То же, что целые числа. Там нет целого числа между 1 и 2.

Там нет никакого способа представить 1/3 как поплавок. Под ним есть поплавок, над ним есть поплавок, и между ними есть определенное расстояние. И 1/3 находится в этом пространстве.

Apfloat для Java утверждает, что работает с числами с плавающей запятой произвольной точности, но я никогда не использовал его. Наверное, стоит посмотреть. http://www.apfloat.org/apfloat_java/

Подобный вопрос был задан здесь до библиотеки высокой точности Java с плавающей точкой

колос
источник
1

Двойные числа - это приблизительные значения десятичных чисел в вашем исходном коде Java. Вы видите последствия несоответствия между double (который является двоичным значением) и вашим источником (который является десятичным).

Java производит самое близкое двоичное приближение. Вы можете использовать java.text.DecimalFormat для отображения лучшего десятичного значения.

С. Лотт
источник
1

Используйте BigDecimal. Он даже позволяет вам задать правила округления (например, ROUND_HALF_EVEN, которые минимизируют статистическую ошибку, округляя до четного соседа, если оба имеют одинаковое расстояние; то есть оба с 1,5 и 2,5 округляют до 2).

Адам Яскевич
источник
1

Краткий ответ: всегда используйте BigDecimal и убедитесь, что вы используете конструктор с аргументом String , а не с двойным.

Возвращаясь к вашему примеру, следующий код напечатает 11.4, как вы пожелаете.

public class doublePrecision {
    public static void main(String[] args) {
      BigDecimal total = new BigDecimal("0");
      total = total.add(new BigDecimal("5.6"));
      total = total.add(new BigDecimal("5.8"));
      System.out.println(total);
    }
}
jackycflau
источник
0

Посмотрите BigDecimal, он решает проблемы, связанные с такой арифметикой с плавающей точкой.

Новый вызов будет выглядеть так:

term[number].coefficient.add(co);

Используйте setScale (), чтобы установить число десятичных знаков, которые будут использоваться.

Темный Замок
источник
0

Почему бы не использовать метод round () из класса Math?

// The number of 0s determines how many digits you want after the floating point
// (here one digit)
total = (double)Math.round(total * 10) / 10;
System.out.println(total); // prints 11.4
Mr.Cat
источник
0

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

public static double sumDouble(double value1, double value2) {
    double sum = 0.0;
    String value1Str = Double.toString(value1);
    int decimalIndex = value1Str.indexOf(".");
    int value1Precision = 0;
    if (decimalIndex != -1) {
        value1Precision = (value1Str.length() - 1) - decimalIndex;
    }

    String value2Str = Double.toString(value2);
    decimalIndex = value2Str.indexOf(".");
    int value2Precision = 0;
    if (decimalIndex != -1) {
        value2Precision = (value2Str.length() - 1) - decimalIndex;
    }

    int maxPrecision = value1Precision > value2Precision ? value1Precision : value2Precision;
    sum = value1 + value2;
    String s = String.format("%." + maxPrecision + "f", sum);
    sum = Double.parseDouble(s);
    return sum;
}
user5734382
источник
-1

Не тратьте свои силы, используя BigDecimal. В 99,9999% случаев вам это не нужно. Тип java double является приблизительным, но почти во всех случаях он достаточно точен. Имейте в виду, что у вас есть ошибка в 14-й значащей цифре.Это действительно незначительно!

Чтобы получить хороший результат, используйте:

System.out.printf("%.2f\n", total);
Maciek D.
источник
2
Я думаю, что он обеспокоен выходом, а не числовой точностью. и BigDecimal не поможет, если вы, например. разделить на три. Это может даже усугубить ситуацию ...
Maciek D.
Вы никогда не должны никогда использовать плавающую точку для денег. Я видел крупную переделку, навязанную подрядчику, который нарушил это правило, несмотря на то, что получил наставления.
маркиз Лорн