Зберігайте точність з подвійною в 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?


Відповіді:


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 для BigDecimalкласу:

Незмінні, довільно точні підписані десяткові числа. BigDecimal складається з довільного значення довільної точності цілого числа та 32-бітової цілочисельної шкали. Якщо нуль або додатник, шкала - це число цифр праворуч від десяткової коми. Якщо від'ємне значення, незазначене значення числа множиться на десять на потужність заперечення шкали. Значення числа, представленого BigDecimal, є (невизначена величина × 10 ^ -масштаб).

Про "Переповнення стека" було багато питань, що стосуються питання чисел з плаваючою комою та його точності. Ось перелік пов’язаних питань, які можуть вас зацікавити:

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


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у цьому випадку, воно не потрібне, оскільки у подвійних є 15 точних знаків точності, вам просто потрібно округлення.
Пітер Лорі

2
@PeterLawrey Він має 15 десяткових цифр точності, якщо вони всі перед десятковою комою. Після десяткової крапки може статися все, через незмінність десяткових і двійкових дробів.
Маркіз Лорн

@EJP Ви праві, він має близько 15 значущих цифр точності. Це може бути 16, але безпечніше припустити, що це 15, а може і 14.
Пітер Лоурі

@PeterLawrey Виправлення EJP було пов’язано з моїм запитанням: stackoverflow.com/questions/36344758/…. Чи можете ви, будь ласка, розширити питання, чому його точно не 15 та в яких ситуаціях це може бути 16 чи 14?
Shivam Sinha

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

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

Запишіть методи додавання, віднімання, множення та ділення, а також метод toDouble. Таким чином ви можете уникнути поплавків під час розрахунків.

EDIT: Швидка реалізація,

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: Він також потенційно вводить помилку з плаваючою комою під час роботи з математичними операціями. Зважаючи на те, що суть цієї вправи полягає у тому, щоб уникнути саме цього, здається доцільним її змінити.
Самір Тальвар

Редаговано для використання ints замість подвійних з причин, зазначених Саміром Тальваром вище.
Вірусний шах

3
Ця реалізація дробів має проблеми, оскільки вона не зводить їх до найпростішої форми. 2/3 * 1/2 дайте 2/6 там, де ви хочете, щоб відповідь була 1/3. В ідеалі в конструкторі ви хочете знайти gcd чисельника і дільник і розділити обидва на це.
Salix alba

15

Зверніть увагу, що у вас була б така ж проблема, якби ви використовували десяткову арифметику з обмеженою точністю і хотіли мати справу з 1/3: 0,333333333 * 3 - це 0,999999999, а не 1 000 000 000.

На жаль, 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
Ні, не використовуйте подвійний з грошовими значеннями! Вам потрібна точність з грошима, замість цього використовуйте BigDecimal. Інакше ваша відповідь хороша. Все, що вам потрібно для точності, використовуйте BigDecimal, якщо точність не така важлива, ви можете використовувати float або double.
MetroidFan2002

1
Питання більше не говорить або не означає, що гроші задіяні. Я спеціально кажу, щоб використовувати BigDecimal або цілі числа для грошей. В чому проблема?
Стів Джессоп

1
І рівне "не використовувати подвійний для грошей" - це "не використовувати BigDecimal або подвійний для третин". Але іноді проблема передбачає поділ, і в тому випадку, коли всі бази, що не поділяються всіма простими факторами всіх знаменників, приблизно однаково погані.
Стів Джессоп

1
.9999 = 1, якщо ваша точність менше 4 значущих цифр
Brian Leahy

9

Ви можете вивчити використання класу java.math.BigDecimal Java, якщо вам дійсно потрібна точність математики. Ось хороша стаття від 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

5

Досить впевнений, що ти міг би зробити це на прикладі трьох рядків. :)

Якщо ви хочете точної точності, використовуйте BigDecimal. В іншому випадку ви можете використовувати ints, помножені на 10 ^, будь-якої точності ви хочете.


5

Як зазначали інші, не всі десяткові значення можуть бути представлені як двійкові, оскільки десятковий базується на потужностях 10, а двійковий - на двох.

Якщо точність має значення, використовуйте BigDecimal, але якщо ви просто бажаєте дружнього результату:

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

Дасть вам:

11.40


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.

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
}

3

Використовуйте java.math.BigDecimal

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


1
-1 для сліпого рекомендування BigDecimal. Якщо вам насправді не потрібна десяткова арифметика (тобто, якщо ви робите розрахунки з грошима), то BigDecimal вам не допоможе. Це не вирішує всіх ваших помилок з плаваючою комою: вам все одно доведеться мати справу з 1/3 * 3 = 0.9999999999999999999999999999 та sqrt (2) ** 2 = 1.99999999999999999999999999999. Крім того, BigDecimal несе величезну швидкість покарання. Гірше, що через відсутність Java перевантаження оператором, вам доведеться переписати весь код.
dan04

2
@ dan04 - Якщо ви робите розрахунок з грошима, навіщо використовувати плаваюче представлення, знаючи притаманну йому помилку .... Оскільки немає частки копійок, ви можете використовувати десятковий і обчислювати центи, а не використовувати приблизний долар, ви маєте точну відсоткову суму. Якщо ви дійсно хочете, щоб частка центів використовувала довгі і обчислюйте тисячі центів. Крім того, ОП не згадувала ірраціональних чисел, все, що його хвилювало, було доповненням. Прочитайте ретельно публікацію і зрозумійте проблему, перш ніж відповісти, це може врятувати вас збентеження.
Ньютопський

3
@Newtopian: Мені немає чого соромитися. ОП не зробив НІЯКОЇ згадка про гроші, ні будь - яких ознак того, що його проблема має ніякого властивого decimalness.
dan04

@ dan04 - Ні ОП не зробив ... ТИ зробив, і сліпо запропонував з контекстної думки те, що, швидше за все, було цілком прийнятною відповіддю з огляду на бідну кількість деталей
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. Ви бачите наслідок невідповідності між подвійним (що є двійковим кодованим значенням) та вашим джерелом (яке кодується десятковим числом).

Java виробляє найближче бінарне наближення. Ви можете використовувати java.text.DecimalFormat для відображення кращого десяткового значення.


1

Використовуйте BigDecimal. Це навіть дозволяє вказати правила округлення (наприклад, ROUND_HALF_EVEN), що дозволить мінімізувати статистичну похибку шляхом округлення до рівномірного сусіда, якщо обидва знаходяться на однаковій відстані;


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);
    }
}

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

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;
}

-1

Не витрачайте зусиль, використовуючи BigDecimal. У 99,99999% випадків вам це не потрібно. подвійний тип java є приблизним, але майже у всіх випадках досить точним. Зверніть увагу, що у вас є помилка на 14-й значній цифрі. Це справді незначно!

Щоб отримати хороший вихід, використовуйте:

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

2
Я думаю, що його турбує результат, а не числова точність. і BigDecimal не допоможе, якщо, наприклад. ділимо на три. Це навіть може погіршити ситуацію ...
Maciek D.

Ви ніколи не повинні ніколи використовувати гроші з плаваючою комою для грошей. Я бачив велику переробку щодо підрядника, який порушив це правило, незважаючи на те, що його так доручили.
Маркіз Лорн
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.