Pertanyaan Bagaimana cara menghindari kesalahan presisi floating point dengan float atau doubles di Java?


Saya memiliki masalah yang sangat mengganggu dengan jumlah lama mengapung atau ganda di Jawa. Ide dasarnya adalah jika saya mengeksekusi:

for ( float value = 0.0f; value < 1.0f; value += 0.1f )
    System.out.println( value );

Yang saya dapatkan adalah:

0.0
0.1
0.2
0.3
0.4
0.5
0.6
0.70000005
0.8000001
0.9000001

Saya mengerti bahwa ada akumulasi dari kesalahan presisi mengambang, bagaimana cara menyingkirkannya? Saya mencoba menggunakan doubles to half the error, tetapi hasilnya masih sama.

Ada ide?


28
2018-03-10 08:35


asal


Jawaban:


Tidak ada perwakilan tepat 0,1 sebagai float atau double. Karena kesalahan representasi ini hasilnya sedikit berbeda dari yang Anda harapkan.

Beberapa pendekatan yang dapat Anda gunakan:

  • Saat menggunakan double ketik, hanya menampilkan digit sebanyak yang Anda butuhkan. Ketika memeriksa persamaan memungkinkan toleransi kecil dengan cara apa pun.
  • Sebagai alternatif, gunakan jenis yang memungkinkan Anda menyimpan nomor yang Anda coba wakili dengan tepat, misalnya BigDecimal dapat mewakili 0,1 tepat.

Contoh kode untuk BigDecimal:

BigDecimal step = new BigDecimal("0.1");
for (BigDecimal value = BigDecimal.ZERO;
     value.compareTo(BigDecimal.ONE) < 0;
     value = value.add(step)) {
    System.out.println(value);
}

Lihat secara online: ideone


32
2018-03-10 08:36



Anda dapat menghindari masalah khusus ini menggunakan kelas seperti BigDecimal. float dan double, karena IEEE 754 floating-point, tidak dirancang untuk menjadi sangat akurat, mereka dirancang untuk menjadi cepat. Tetapi perhatikan poin Jon di bawah ini: BigDecimal tidak dapat mewakili "sepertiga" secara akurat, lebih dari double dapat mewakili "sepersepuluh" secara akurat. Tetapi untuk (katakanlah) perhitungan keuangan, BigDecimal dan kelas-kelas seperti itu cenderung menjadi cara untuk pergi, karena mereka dapat mewakili angka-angka dalam cara yang kita manusia cenderung pikirkan tentang mereka.


9
2018-03-10 08:37



Jangan gunakan float / double dalam iterator karena ini memaksimalkan kesalahan pembulatan Anda. Jika Anda hanya menggunakan yang berikut ini

for (int i = 0; i < 10; i++)
    System.out.println(i / 10.0);

itu mencetak

0.0
0.1
0.2
0.3
0.4
0.5
0.6
0.7
0.8
0.9

Saya tahu BigDecimal adalah pilihan yang populer, tetapi saya lebih suka ganda bukan karena jauh lebih cepat tetapi biasanya lebih pendek / lebih bersih untuk dipahami.

Jika Anda menghitung jumlah simbol sebagai ukuran kompleksitas kode

  • menggunakan double => 11 simbol
  • gunakan BigDecimal (dari @Mark Byers contoh) => 21 simbol

BTW: jangan gunakan float kecuali ada sangat alasan bagus untuk tidak menggunakan double.


8
2018-03-10 09:20



Nya tidak hanya akumulasi kesalahan (dan sama sekali tidak ada hubungannya dengan Java). 1.0f, setelah diterjemahkan ke kode aktual, tidak memiliki nilai 0,1 - Anda sudah mendapatkan kesalahan pembulatan.

Dari Panduan Floating-Point:

Apa yang dapat saya lakukan untuk menghindari masalah ini?

Itu tergantung pada jenis apa   perhitungan yang Anda lakukan.

  • Jika Anda benar-benar membutuhkan hasil Anda untuk bertambah tepat, terutama ketika Anda bekerja dengan uang: gunakan datatype desimal khusus.
  • Jika Anda tidak ingin melihat semua tempat desimal tambahan: cukup format hasil Anda dibulatkan menjadi tetap   jumlah tempat desimal saat   menampilkannya.
  • Jika Anda tidak memiliki datatype desimal, alternatifnya adalah bekerja   dengan bilangan bulat, mis. lakukan uang   perhitungan sepenuhnya dalam sen. Tapi   ini lebih banyak pekerjaan dan memiliki beberapa   kemunduran.

Baca situs yang terhubung ke situs untuk mendapatkan informasi terperinci.


3
2018-03-10 08:40



Demi kelengkapan saya merekomendasikan yang satu ini:

Shewchuck, "Predikat Geometris Predikat Kuat Adaptif-Titik", jika Anda ingin lebih banyak contoh tentang bagaimana melakukan aritmatika yang tepat dengan titik mengambang - atau setidaknya akurasi terkontrol yang merupakan niat asli penulis, http://www.cs.berkeley.edu/~jrs/papers/robustr.pdf 


3
2017-11-16 20:28



Solusi lain adalah melupakan == dan periksa apakah kedua nilai itu cukup dekat. (Saya tahu ini bukan yang Anda minta di dalam tubuh tetapi saya menjawab judul pertanyaan.)


2
2018-03-10 08:45



Saya telah menghadapi masalah yang sama, memutuskan hal yang sama menggunakan BigDecimal. Di bawah ini adalah potongan yang membantu saya.

double[] array = {45.34d, 45000.24d, 15000.12d, 4534.89d, 3444.12d, 12000.00d, 4900.00d, 1800.01d};
double total = 0.00d;
BigDecimal bTotal = new BigDecimal(0.0+"");
for(int i = 0;i < array.length; i++) {
    total += (double)array[i];
    bTotal = bTotal.add(new BigDecimal(array[i] +""));
}
System.out.println(total);
System.out.println(bTotal);

Semoga ini akan membantu Anda.


2
2017-07-24 06:39



Anda harus menggunakan datatype desimal, bukan pelampung:

https://docs.oracle.com/javase/7/docs/api/java/math/BigDecimal.html


2
2018-03-10 08:37



package loopinamdar;

import java.text.DecimalFormat;

public class loopinam {
    static DecimalFormat valueFormat = new DecimalFormat("0.0");

    public static void main(String[] args) {
        for (float value = 0.0f; value < 1.0f; value += 0.1f)
            System.out.println("" + valueFormat.format(value));
    }
}

2
2018-05-12 20:07



Pertama membuatnya a dua kali lipat. Jangan pernah digunakan mengapung atau Anda akan kesulitan menggunakan java.lang.Math utilitas.

Sekarang jika Anda kebetulan tahu sebelumnya presisi Anda ingin dan itu sama atau kurang dari 15, maka itu menjadi mudah untuk memberitahu Anda dua kali lipats berperilaku. Periksa di bawah ini:

// the magic method:
public final static double makePrecise(double value, int precision) {
    double pow = Math.pow(10, precision);
    long powValue = Math.round(pow * value);
    return powValue / pow;
}

Sekarang setiap kali Anda melakukan operasi, Anda harus memberi tahu Anda dua kali lipat hasil untuk berperilaku:

for ( double value = 0.0d; value < 1.0d; value += 0.1d )
            System.out.println( makePrecise(value, 1) + " => " + value );

Keluaran:

0.0 => 0.0
0.1 => 0.1
0.2 => 0.2
0.3 => 0.30000000000000004
0.4 => 0.4
0.5 => 0.5
0.6 => 0.6
0.7 => 0.7
0.8 => 0.7999999999999999
0.9 => 0.8999999999999999
1.0 => 0.9999999999999999

Jika Anda membutuhkan lebih dari 15 presisi maka Anda kurang beruntung:

for ( double value = 0.0d; value < 1.0d; value += 0.1d )
            System.out.println( makePrecise(value, 16) + " => " + value );

Keluaran:

0.0 => 0.0
0.1 => 0.1
0.2 => 0.2
0.3000000000000001 => 0.30000000000000004
0.4 => 0.4
0.5 => 0.5
0.6 => 0.6
0.7 => 0.7
0.8 => 0.7999999999999999
0.9 => 0.8999999999999999
0.9999999999999998 => 0.9999999999999999

CATATAN 1: Untuk kinerja, Anda harus menyimpan cache Math.pow operasi dalam array. Tidak dilakukan di sini untuk kejelasan.

CATATAN 2: Itu sebabnya kami tidak pernah menggunakannya dua kali lipatuntuk harga, tapi panjangs di mana N terakhir (yaitu di mana N <= 15, biasanya 8) digit adalah digit desimal. Maka Anda bisa melupakan apa yang saya tulis di atas :)


1
2018-04-13 20:31