Mempertahankan presisi dengan ganda di Jawa

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

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

Kode di atas mencetak:

11.399999999999

Bagaimana saya bisa mencetak ini (atau bisa menggunakannya) 11.4?

Deinumite
sumber
1
Terkait: Apakah matematika floating point rusak?
Bernhard Barker

Jawaban:

151

Seperti yang disebutkan orang lain, Anda mungkin ingin menggunakan BigDecimal kelas, jika Anda ingin memiliki representasi 11.4.

Sekarang, sedikit penjelasan mengapa ini terjadi:

The floatdan doubleprimitif jenis di Jawa floating point nomor, di mana jumlah disimpan sebagai representasi biner dari fraksi dan eksponen a.

Lebih khusus lagi, nilai floating point presisi ganda seperti doublejenisnya adalah nilai 64-bit, di mana:

  • 1 bit menunjukkan tanda (positif atau negatif).
  • 11 bit untuk eksponen.
  • 52 bit untuk digit signifikan (bagian pecahan sebagai biner).

Bagian-bagian ini digabungkan untuk menghasilkan doublerepresentasi suatu nilai.

(Sumber: Wikipedia: Presisi ganda )

Untuk uraian terperinci tentang bagaimana nilai-nilai titik mengambang ditangani di Jawa, lihat Bagian 4.2.3: Jenis Titik Apung, Format, dan Nilai Spesifikasi Bahasa Jawa.

The byte, char, int, longjenis yang fixed-point nomor, yang representions yang tepat dari angka. Tidak seperti angka titik tetap, angka titik mengambang akan beberapa kali (aman untuk menganggap "sebagian besar waktu") tidak dapat mengembalikan representasi angka yang tepat. Ini adalah alasan mengapa Anda berakhir dengan 11.399999999999sebagai hasil dari 5.6 + 5.8.

Saat membutuhkan nilai yang tepat, seperti 1.5 atau 150.1005, Anda akan ingin menggunakan salah satu tipe titik tetap, yang akan dapat mewakili angka dengan tepat.

Seperti yang telah disebutkan beberapa kali, Java memiliki a BigDecimal kelas yang akan menangani jumlah yang sangat besar dan jumlah yang sangat kecil.

Dari Referensi Java API untuk BigDecimalkelas:

Angka desimal bertanda arbitrer dan presisi sewenang-wenang. BigDecimal terdiri dari nilai unscaled integer presisi sewenang-wenang dan skala integer 32-bit. Jika nol atau positif, skala adalah jumlah digit di sebelah kanan titik desimal. Jika negatif, nilai unscaled dari angka dikalikan sepuluh dengan kekuatan negasi skala. Nilai angka yang diwakili oleh BigDecimal karenanya (unscaledValue × 10 ^ -scale).

Ada banyak pertanyaan tentang Stack Overflow terkait dengan masalah angka floating point dan ketepatannya. Berikut adalah daftar pertanyaan terkait yang mungkin menarik:

Jika Anda benar-benar ingin pergi ke detail seluk beluk nomor floating point, lihat Apa Yang Setiap Ilmuwan Komputer Harus Tahu Tentang Aritmatika Floating-Point .

coobird
sumber
3
Bahkan, biasanya ada 53 bit signifikan karena 1 sebelum titik "desimal" tersirat untuk semua kecuali nilai-nilai yang dinormalisasi, memberikan bit presisi tambahan. misal 3 disimpan sebagai (1.) 1000 ... x 2 ^ 1 sementara 0,5 disimpan sebagai (1.) 0000 ... x 2 ^ -1 Ketika nilainya dinormalisasi (semua bit eksponen adalah nol) dapat, dan biasanya akan, lebih sedikit digit signifikan mis. 1 x 2 ^ -1030 disimpan sebagai (0.) 00000001 x 2 ^ -1022 sehingga tujuh digit signifikan telah dikorbankan untuk skala.
Sarah Phillips
1
Perlu dicatat bahwa sementara BigDecimaljauh lebih lambat daripada doubledalam kasus ini tidak diperlukan karena ganda memiliki 15 tempat desimal akurasi, Anda hanya perlu pembulatan.
Peter Lawrey
2
@PeterLawrey Ini memiliki 15 digit angka desimal , jika semuanya sebelum titik desimal. Apa pun dapat terjadi setelah titik desimal, karena ketidakterbandingan dari fraksi desimal dan biner.
Marquis dari Lorne
@ EJP Anda benar, ia memiliki sekitar 15 digit presisi yang signifikan. Bisa jadi 16 tetapi lebih aman untuk menganggapnya 15 atau mungkin 14.
Peter Lawrey
@PeterLawrey EJP koreksi adalah karena pertanyaan saya: stackoverflow.com/questions/36344758/... bisakah Anda memperluas mengapa ini tidak tepat 15 dan situasi apa yang bisa 16 atau 14?
Shivam Sinha
103

Ketika Anda memasukkan angka ganda, misalnya, 33.33333333333333nilai yang Anda dapatkan sebenarnya adalah nilai presisi ganda yang dapat direpresentasikan, yaitu:

33.3333333333333285963817615993320941925048828125

Membagi dengan 100 memberi:

0.333333333333333285963817615993320941925048828125

yang juga tidak dapat direpresentasikan sebagai angka presisi ganda, jadi sekali lagi itu dibulatkan ke nilai keterwakilan terdekat, yaitu:

0.3333333333333332593184650249895639717578887939453125

Ketika Anda mencetak nilai ini, itu akan dibulatkan lagi menjadi 17 angka desimal, memberikan:

0.33333333333333326
Stephen Canon
sumber
114
Bagi siapa pun yang membaca ini di masa depan dan bingung mengapa jawabannya tidak ada hubungannya dengan pertanyaan: beberapa moderator memutuskan untuk menggabungkan pertanyaan yang saya (dan yang lain) jawab dengan pertanyaan ini, yang agak berbeda.
Stephen Canon
Bagaimana Anda tahu nilai ganda yang tepat?
Michael Yaworski
@mikeyaworski en.wikipedia.org/wiki/Double-precision_floating-point_format Lihat contoh presisi ganda
Jaydee
23

Jika Anda hanya ingin memproses nilai sebagai pecahan, Anda bisa membuat kelas pecahan yang berisi bidang pembilang dan penyebut.

Tulis metode untuk menambah, mengurangi, mengalikan, dan membagi serta metode toDouble. Dengan cara ini Anda dapat menghindari mengapung saat perhitungan.

EDIT: Implementasi cepat,

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

}
Viral Shah
sumber
1
Tentunya numeratordan denominatorharus ints? Mengapa Anda ingin presisi floating-point?
Samir Talwar
Kira itu tidak benar-benar diperlukan tetapi menghindari casting di fungsi toDouble sehingga kode dibaca lebih baik.
Viral Shah
5
ViralShah: Ini juga berpotensi memperkenalkan kesalahan floating-point ketika berhadapan dengan operasi matematika. Mengingat bahwa inti dari latihan ini adalah untuk menghindari hal itu, tampaknya bijaksana untuk mengubahnya.
Samir Talwar
Diedit untuk menggunakan int bukan ganda, untuk alasan yang disebutkan oleh Samir Talwar di atas.
Viral Shah
3
Implementasi fraksi ini memiliki masalah karena tidak menguranginya menjadi bentuk yang paling sederhana. 2/3 * 1/2 beri 2/6 di mana Anda benar-benar ingin jawabannya 1/3. Idealnya di konstruktor Anda ingin menemukan gcd pembilang dan pembagi dan membagi keduanya dengan itu.
Salix alba
15

Perhatikan bahwa Anda akan memiliki masalah yang sama jika Anda menggunakan aritmatika desimal presisi-terbatas, dan ingin berurusan dengan 1/3: 0,333333333 * 3 adalah 0,999999999, bukan 1,00000000.

Sayangnya, 5.6, 5.8 dan 11.4 hanya bukan angka bulat dalam biner, karena mereka melibatkan perlima. Jadi representasi float dari mereka tidak tepat, sama seperti 0,3333 tidak tepat 1/3.

Jika semua angka yang Anda gunakan adalah desimal yang tidak berulang, dan Anda menginginkan hasil yang pasti, gunakan BigDecimal. Atau seperti yang dikatakan orang lain, jika nilai Anda seperti uang dalam arti bahwa mereka semua kelipatan 0,01, atau 0,001, atau sesuatu, maka gandakan semuanya dengan kekuatan tetap 10 dan gunakan int atau panjang (penambahan dan pengurangan adalah sepele: hati-hati terhadap multiplikasi).

Namun, jika Anda senang dengan perhitungan biner, tetapi Anda hanya ingin mencetak beberapa hal dalam format yang sedikit lebih ramah, coba java.util.Formatteratau String.format. Dalam format string, tentukan presisi kurang dari ketepatan penuh ganda. Untuk 10 angka penting, katakanlah, 11.399999999999 adalah 11,4, sehingga hasilnya akan hampir seakurat dan lebih dapat dibaca manusia dalam kasus di mana hasil biner sangat dekat dengan nilai yang hanya membutuhkan beberapa tempat desimal.

Ketepatan untuk menentukan sedikit tergantung pada berapa banyak matematika yang telah Anda lakukan dengan angka-angka Anda - secara umum semakin banyak Anda lakukan, semakin banyak kesalahan akan terakumulasi, tetapi beberapa algoritma menumpuknya lebih cepat daripada yang lain (mereka disebut "tidak stabil" sebagai menentang "stable" sehubungan dengan kesalahan pembulatan). Jika semua yang Anda lakukan adalah menambahkan beberapa nilai, maka saya kira bahwa menjatuhkan hanya satu tempat desimal presisi akan menyelesaikan masalah. Percobaan.

Steve Jessop
sumber
3
Tidak, jangan tidak menggunakan ganda dengan nilai-nilai moneter! Anda membutuhkan ketepatan dengan uang, gunakan BigDecimal sebagai gantinya. Kalau tidak, jawaban Anda baik. Apa pun yang Anda butuhkan presisi, gunakan BigDecimal, jika presisi tidak terlalu penting, Anda dapat menggunakan float atau double.
MetroidFan2002
1
Pertanyaannya tidak lagi menyatakan atau menyiratkan bahwa uang terlibat. Saya secara khusus mengatakan untuk menggunakan BigDecimal atau bilangan bulat untuk uang. Apa masalahnya?
Steve Jessop
1
Dan sama dengan "jangan gunakan dobel untuk uang" adalah "jangan gunakan BigDecimal atau dobel untuk pertiga". Tetapi kadang-kadang masalah melibatkan perpecahan, di mana semua pangkalan tidak dibagi oleh semua faktor utama dari semua penyebut sama buruknya.
Steve Jessop
1
0,9999 = 1 jika ketepatan Anda kurang dari 4 digit signifikan
Brian Leahy
9

Anda mungkin ingin melihat menggunakan kelas java.math.BigDecimal java jika Anda benar-benar membutuhkan matematika presisi. Berikut ini adalah artikel bagus dari Oracle / Sun tentang kasus untuk BigDecimal . Meskipun Anda tidak pernah bisa mewakili 1/3 sebagai seseorang yang disebutkan, Anda bisa memiliki kekuatan untuk memutuskan secara tepat seberapa Anda inginkan hasilnya. setScale () adalah teman Anda .. :)

Ok, karena saya punya terlalu banyak waktu di tangan saya saat ini di sini adalah contoh kode yang berkaitan dengan pertanyaan Anda:

import java.math.BigDecimal;
/**
 * Created by a wonderful programmer known as:
 * Vincent Stoessel
 * [email protected]
 * 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


    }
}

dan untuk memasukkan bahasa favorit baru saya, Groovy, berikut adalah contoh yang lebih rapi dari hal yang sama:

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
sumber
5

Cukup yakin Anda bisa membuatnya menjadi contoh tiga baris. :)

Jika Anda ingin presisi yang tepat, gunakan BigDecimal. Jika tidak, Anda dapat menggunakan int dikalikan dengan 10 ^ presisi apa pun yang Anda inginkan.

Dustin
sumber
5

Seperti yang telah dicatat oleh orang lain, tidak semua nilai desimal dapat direpresentasikan sebagai biner karena desimal didasarkan pada pangkat 10 dan biner didasarkan pada pangkat dua.

Jika masalah presisi, gunakan BigDecimal, tetapi jika Anda hanya ingin hasil yang ramah:

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

Akan memberimu:

11.40
Draemon
sumber
5

Anda tidak bisa, karena 7.3 tidak memiliki representasi terbatas dalam biner. Yang terdekat yang bisa Anda dapatkan adalah 2054767329987789/2 ** 48 = 7.3 + 1/1407374883553280.

Lihatlah http://docs.python.org/tutorial/floatingpoint.html untuk penjelasan lebih lanjut. (Ada di situs web Python, tetapi Java dan C ++ memiliki "masalah" yang sama.)

Solusinya tergantung pada apa sebenarnya masalah Anda:

  • Jika Anda tidak suka melihat semua digit derau itu, maka perbaiki pemformatan string Anda. Jangan tampilkan lebih dari 15 digit signifikan (atau 7 untuk float).
  • Jika itu karena ketidaktepatan angka Anda melanggar hal-hal seperti pernyataan "jika", maka Anda harus menulis if (abs (x - 7.3) <TOLERANCE) alih-alih if (x == 7.3).
  • Jika Anda bekerja dengan uang, maka yang mungkin Anda inginkan adalah titik tetap desimal. Menyimpan bilangan bulat sen atau apapun unit terkecil dari mata uang Anda.
  • (SANGAT TIDAK MUNCUL) Jika Anda membutuhkan lebih dari 53 bit signifikan (15-16 digit signifikan) presisi, maka gunakan tipe floating-point presisi tinggi, seperti BigDecimal.
dan04
sumber
7.3 mungkin tidak memiliki representasi terbatas dalam biner, tapi saya yakin mendapatkan -7,3 ketika saya mencoba hal yang sama di C ++
wrongusername
2
nama pengguna salah: Tidak, Anda tidak. Itu hanya menampilkan seperti itu. Gunakan format "% .17g" (atau lebih baik lagi, "% .51g") untuk melihat jawaban yang sebenarnya.
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
sumber
3

Gunakan java.math.BigDecimal

Doubles adalah fraksi biner secara internal, sehingga mereka terkadang tidak dapat merepresentasikan fraksi desimal ke desimal yang tepat.

Kevin Crowell
sumber
1
-1 untuk merekomendasikan BigDecimal secara membabi buta. Jika Anda tidak benar-benar membutuhkan aritmatika desimal (yaitu, jika Anda melakukan perhitungan dengan uang), maka BigDecimal tidak membantu Anda. Itu tidak menyelesaikan semua kesalahan floating-point Anda: Anda masih harus berurusan dengan 1/3 * 3 = 0,999999999999999999999999999999 dan sqrt (2) ** 2 = 1.99999999999999999999999999999. Selanjutnya, BigDecimal membawa penalti kecepatan besar. Lebih buruk lagi, karena kurangnya kelebihan operator di Jawa, Anda harus menulis ulang semua kode Anda.
dan04
2
@ dan04 - Jika Anda melakukan perhitungan dengan uang mengapa menggunakan representasi mengambang mengetahui kesalahan yang melekat di dalamnya .... Karena tidak ada pecahan sen, Anda dapat menggunakan desimal dan menghitung sen, alih-alih menggunakan perkiraan dolar, Anda memiliki jumlah sen yang tepat. Jika Anda benar-benar menginginkan pecahan sen, gunakan panjang dan hitung ribuan sen. Lebih jauh lagi OP tidak menyebutkan angka irasional, yang dia khawatirkan adalah penambahan. Baca posting dengan hati-hati dan pahami masalahnya sebelum Anda menjawab, mungkin akan membuat Anda merasa malu.
Newtopian
3
@Newtopian: Saya tidak perlu merasa malu. OP tidak menyebutkan uang, juga tidak ada indikasi bahwa masalahnya memiliki desimal yang melekat.
dan04
@ dan04 - Tidak OP tidak ... ANDA lakukan Dan Blindly menawarkan di luar konteks pendapat untuk apa yang kemungkinan besar merupakan jawaban yang dapat diterima mengingat jumlah detail yang disediakan
Newtopian
2

Lipat gandakan semuanya menjadi 100 dan simpan dalam sen.

Paul Tomblin
sumber
2
@Raemon - lihat posting sebelum edit terakhir - semua hal "shoppingTotal" dan "calcGST" dan "calcPST" terlihat seperti uang bagi saya.
Paul Tomblin
2

Komputer menyimpan angka dalam biner dan tidak bisa benar-benar mewakili angka seperti 33.333333333 atau 100.0 tepatnya. Ini adalah salah satu hal rumit tentang menggunakan ganda. Anda harus memutari jawaban sebelum menunjukkannya kepada pengguna. Untungnya di sebagian besar aplikasi, Anda tidak perlu banyak tempat desimal.

Jay Askren
sumber
Saya sedang melakukan beberapa perhitungan odds saya lebih suka memiliki presisi setinggi mungkin. Tapi saya mengerti ada batasan
Aly
2

Angka floating point berbeda dari bilangan real karena untuk setiap angka floating point yang diberikan ada angka floating point berikutnya yang lebih tinggi. Sama seperti bilangan bulat. Tidak ada bilangan bulat antara 1 dan 2.

Tidak ada cara untuk mewakili 1/3 sebagai pelampung. Ada pelampung di bawahnya dan ada pelampung di atasnya, dan ada jarak tertentu di antara mereka. Dan 1/3 ada di ruang itu.

Apfloat untuk Java mengklaim dapat bekerja dengan angka floating point presisi yang sewenang-wenang, tetapi saya tidak pernah menggunakannya. Mungkin patut dilihat. http://www.apfloat.org/apfloat_java/

Pertanyaan serupa diajukan di sini sebelum Java floating point perpustakaan presisi tinggi

Paku
sumber
1

Doubles adalah perkiraan angka desimal di sumber Java Anda. Anda melihat konsekuensi dari ketidakcocokan antara ganda (yang merupakan nilai kode biner) dan sumber Anda (yang kode desimal).

Java memproduksi perkiraan biner terdekat. Anda dapat menggunakan java.text.DecimalFormat untuk menampilkan nilai desimal yang terlihat lebih baik.

S.Lott
sumber
1

Gunakan BigDecimal. Bahkan memungkinkan Anda menentukan aturan pembulatan (seperti ROUND_HALF_EVEN, yang akan meminimalkan kesalahan statistik dengan membulatkan ke tetangga genap jika keduanya memiliki jarak yang sama; yaitu putaran 1,5 dan 2,5 ke 2).

Adam Jaskiewicz
sumber
1

Jawaban singkat: Selalu gunakan BigDecimal dan pastikan Anda menggunakan konstruktor dengan String argumen , bukan yang ganda.

Kembali ke contoh Anda, kode berikut akan mencetak 11.4, sesuai keinginan.

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
sumber
0

Lihat BigDecimal, ia menangani masalah berurusan dengan aritmatika floating point seperti itu.

Panggilan baru akan terlihat seperti ini:

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

Gunakan setScale () untuk mengatur jumlah presisi tempat desimal yang akan digunakan.

Dark Castle
sumber
0

Mengapa tidak menggunakan metode round () dari kelas 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
sumber
0

Jika Anda tidak punya pilihan selain menggunakan nilai ganda, dapat menggunakan kode di bawah ini.

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;
}
pengguna5734382
sumber
-1

Jangan sia-siakan usaha Anda menggunakan BigDecimal. Dalam 99,99999% kasus Anda tidak membutuhkannya. tipe java ganda adalah perkiraan kasar tetapi dalam hampir semua kasus, itu cukup tepat. Ingatlah bahwa Anda memiliki kesalahan pada digit signifikan ke-14.Ini benar-benar dapat diabaikan!

Untuk mendapatkan hasil yang bagus gunakan:

System.out.printf("%.2f\n", total);
Maciek D.
sumber
2
Saya pikir dia khawatir dengan output, bukan ketepatan angka. dan BigDecimal tidak akan membantu jika Anda mis. mempertiga. Bahkan dapat memperburuk keadaan ...
Maciek D.
Anda seharusnya tidak pernah menggunakan floating-point untuk uang. Saya telah melihat pengerjaan ulang besar-besaran dilakukan pada kontraktor yang melanggar aturan ini meskipun telah diinstruksikan.
Marquis dari Lorne