web-dev-qa-db-fra.com

Comment éviter les erreurs de précision en virgule flottante avec des flottants ou des doubles en Java?

J'ai un problème très ennuyeux avec de longues sommes de flotteurs ou de doubles en Java. Fondamentalement, l'idée est que si j'exécute:

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

Ce que je reçois est:

0.0
0.1
0.2
0.3
0.4
0.5
0.6
0.70000005
0.8000001
0.9000001

Je comprends qu'il y a une accumulation de l'erreur de précision flottante, cependant, comment s'en débarrasser? J'ai essayé d'utiliser le double de la moitié de l'erreur, mais le résultat est toujours le même.

Des idées?

30
tunnuz

Il n'y a pas de représentation exacte de 0,1 comme float ou double. En raison de cette erreur de représentation, les résultats sont légèrement différents de ce que vous attendiez.

Quelques approches que vous pouvez utiliser:

  • Lorsque vous utilisez le type double, n'affichez que le nombre de chiffres dont vous avez besoin. Lors de la vérification de l'égalité, autorisez une petite tolérance dans les deux cas.
  • Vous pouvez également utiliser un type qui vous permet de stocker les nombres que vous essayez de représenter exactement, par exemple BigDecimal peut représenter 0,1 exactement.

Exemple de code pour 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);
}

Voir en ligne: ideone

33
Mark Byers

Vous pouvez éviter ce problème spécifique en utilisant des classes comme BigDecimal . float et double, étant à virgule flottante IEEE 754, ne sont pas conçus pour être parfaitement précis, ils sont conçus pour être rapides. Mais notez le point de Jon ci-dessous: BigDecimal ne peut pas représenter "un tiers" avec précision, pas plus que double ne peut représenter avec précision "un dixième". Mais pour (disons) les calculs financiers, BigDecimal et des classes comme celle-ci ont tendance à être la voie à suivre, car ils peuvent représenter des nombres de la façon dont nous, les humains, avons tendance à penser à eux.

9
T.J. Crowder

N'utilisez pas float/double dans un itérateur car cela maximise votre erreur d'arrondi. Si vous utilisez simplement les éléments suivants

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

il imprime

0.0
0.1
0.2
0.3
0.4
0.5
0.6
0.7
0.8
0.9

Je sais que BigDecimal est un choix populaire, mais je préfère doubler non pas parce que c'est beaucoup plus rapide mais c'est généralement beaucoup plus court/plus propre à comprendre.

Si vous comptez le nombre de symboles comme mesure de la complexité du code

  • en utilisant double => 11 symboles
  • utiliser BigDecimal (à partir de l'exemple @Mark Byers) => 21 symboles

BTW: n'utilisez pas float sauf s'il y a vraiment une bonne raison de ne pas utiliser double.

7
Peter Lawrey

Ce n'est pas juste une erreur accumulée (et n'a absolument rien à voir avec Java). 1.0f, une fois traduit en code réel, n'a pas la valeur 0,1 - vous obtenez déjà une erreur d'arrondi.

De Le Guide à virgule flottante:

Que puis-je faire pour éviter ce problème?

Cela dépend du type de calculs que vous effectuez.

  • Si vous avez vraiment besoin que vos résultats s'additionnent exactement, surtout lorsque vous travaillez avec de l'argent: utilisez un type de données décimal spécial.
  • Si vous ne voulez tout simplement pas voir toutes ces décimales supplémentaires: formatez simplement votre résultat arrondi à un nombre fixe de décimales lors de son affichage.
  • Si vous n'avez pas de type de données décimal disponible, une alternative est de travailler avec des entiers, par ex. faire des calculs d'argent entièrement en cents. Mais cela demande plus de travail et présente certains inconvénients.

Lisez le site lié pour des informations détaillées.

4
Michael Borgwardt

Par souci d'exhaustivité, je recommande celui-ci:

Shewchuck, "Robust Adaptive Floating-Point Geometric Predicates", si vous voulez plus d'exemples sur la façon d'effectuer une arithmétique exacte avec virgule flottante - ou au moins une précision contrôlée qui est l'intention originale de l'auteur, http: // www. cs.berkeley.edu/~jrs/papers/robustr.pdf

3
aka.nice

Une autre solution consiste à renoncer à == et vérifiez si les deux valeurs sont assez proches. (Je sais que ce n'est pas ce que vous avez demandé dans le corps, mais je réponds au titre de la question.)

2
aib

J'avais rencontré le même problème, résolu le même en utilisant BigDecimal. Voici l'extrait de code qui m'a aidé.

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

J'espère que cela vous aidera.

2
Balu SKT
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
Solus

Vous devez utiliser un type de données décimal et non flottant:

https://docs.Oracle.com/javase/7/docs/api/Java/math/BigDecimal.html

2

Faites-en d'abord un double. N'utilisez jamais float ou vous aurez du mal à utiliser le Java.lang.Math utilitaires.

Maintenant, si vous connaissez à l'avance la précision que vous voulez et qu'elle est égale ou inférieure à 15, alors il devient facile de dire à votre double s se comporter. Vérifiez ci-dessous:

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

Maintenant, chaque fois que vous effectuez une opération, vous devez indiquer à votre double résultat de se comporter:

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

Production:

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

Si vous avez besoin de plus de 15 précision, vous n'avez pas de chance:

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

Production:

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

NOTE1: Pour les performances, vous devez mettre en cache le Math.pow opération dans un tableau. Pas fait ici pour plus de clarté.

NOTE2: C'est pourquoi nous n'utilisons jamais double s pour les prix, mais long s où les N derniers (c'est-à-dire où N <= 15, généralement 8) chiffres sont les chiffres décimaux. Ensuite, vous pouvez oublier ce que j'ai écrit ci-dessus :)

1
rdalmeida

Si vous souhaitez continuer à utiliser float et éviter d'accumuler des erreurs en ajoutant à plusieurs reprises 0.1f, essayez quelque chose comme ceci:

for (int count = 0; count < 10; count++) {
    float value = 0.1f * count;
    System.out.println(value);
}

Notez cependant, comme d'autres l'ont déjà expliqué, que float n'est pas un type de données infiniment précis.

0
Jesper

Il vous suffit d'être conscient de la précision requise dans votre calcul et de la précision dont le type de données choisi est capable et de présenter vos réponses en conséquence.

Par exemple, si vous avez affaire à des nombres avec 3 chiffres significatifs, l'utilisation de float (qui fournit une précision de 7 chiffres significatifs) est appropriée. Cependant, vous ne pouvez pas citer votre réponse finale avec une précision de 7 chiffres significatifs si vos valeurs de départ n'ont qu'une précision de 2 chiffres significatifs.

5.01 + 4.02 = 9.03 (to 3 significant figures)

Dans votre exemple, vous effectuez plusieurs ajouts et, avec chaque ajout, il y a un impact conséquent sur la précision finale.

0
Nick Pierpoint