Table of Contents
Introduction
Vous avez peut-être déjà rencontré des problèmes liés à la précision des valeurs flottantes dans vos programmes Java. Les erreurs de décimale peuvent sembler mineures, mais elles peuvent avoir des conséquences importantes sur les résultats de votre application.
Prenons un exemple simple qui illustre parfaitement ce problème :
public class FloatPrecisionDemo {
public static void main(String[] args) {
double result = 0.1 + 0.2;
System.out.println("0.1 + 0.2 = " + result);
System.out.println("Est-ce égal à 0.3 ? " + (result == 0.3));
}
}
Le résultat surprenant :
0.1 + 0.2 = 0.30000000000000004
Est-ce égal à 0.3 ? false
Ce comportement inattendu découle de la représentation binaire des nombres à virgule flottante selon la norme IEEE 754. Dans cet article, nous allons explorer en détail ces problèmes et vous présenter les solutions professionnelles pour éviter les erreurs liées aux valeurs flottantes.
Le problème des valeurs flottantes
Les valeurs flottantes sont représentées sous forme de nombres binaires avec une précision limitée. Voici les caractéristiques de chaque type :
| Type | Bits de mantisse | Précision décimale | Plage de valeurs |
|---|---|---|---|
float | 23 bits | ~7 chiffres | ±3.4 x 10^38 |
double | 52 bits | ~15 chiffres | ±1.7 x 10^308 |
En plus de cela, certaines opérations arithmétiques introduisent des erreurs de décimale. Par conséquent, lorsqu’un programme compare les valeurs flottantes, il est une pratique courante de définir un delta d’acceptabilité pour la comparaison. Si la différence entre les deux nombres est inférieure au delta, ils sont considérés comme égaux.
Comprendre la représentation IEEE 754
La norme IEEE 754 définit comment les nombres à virgule flottante sont stockés en mémoire :
public class IEEE754Demo {
public static void main(String[] args) {
double value = 0.1;
// Afficher la représentation binaire
long bits = Double.doubleToLongBits(value);
System.out.println("Valeur: " + value);
System.out.println("Bits: " + Long.toBinaryString(bits));
// Montrer l'erreur de représentation
System.out.println("Représentation exacte: " +
new java.math.BigDecimal(value));
}
}
Résultat :
Valeur: 0.1
Représentation exacte: 0.1000000000000000055511151231257827021181583404541015625
Exemple: Calcul du delta
public class DeltaCompareExample {
private static boolean deltaCompare(double v1, double v2, double delta) {
// retourne vrai si la différence entre v1 et v2 est inférieure à delta
return Math.abs(v1 - v2) < delta;
}
public static void main(String[] args) {
double[] doubles = {1.0, 1.0001, 1.0000001, 1.000000001, 1.0000000000001};
double[] deltas = {0.01, 0.00001, 0.0000001, 0.0000000001, 0};
// boucle sur tous les deltas
for (int j = 0; j < deltas.length; j++) {
double delta = deltas[j];
System.out.println("delta: " + delta);
// boucle sur tous les doubles
for (int i = 0; i < doubles.length - 1; i++) {
double d1 = doubles[i];
double d2 = doubles[i + 1];
boolean result = deltaCompare(d1, d2, delta);
System.out.println("" + d1 + " == " + d2 + " ? " + result);
}
System.out.println();
}
}
}
Comparaison de valeurs flottantes et décimales
Lorsque vous comparez des valeurs flottantes et décimales, il est possible que les résultats ne soient pas exacts. Pour éviter cela, vous pouvez utiliser la méthode Double.compare() pour comparer deux valeurs.
double a = 1.0;
double b = 1.0001;
System.out.println(Double.compare(a, b)); // -1
System.out.println(Double.compare(b, a)); // 1
Détermination du delta approprié
La détermination du delta approprié pour une comparaison peut être difficile. Une approche courante est de choisir des valeurs de delta qui correspondent à notre intuition. Cependant, si vous connaissez l’échelle et la précision des valeurs d’entrée ainsi que les calculs effectués, il est possible de déterminer mathématiquement les limites de précision des résultats et donc pour les deltas.
Formatage des valeurs flottantes
Les valeurs flottantes peuvent être formatées en utilisant String.format() ou DecimalFormat.
// deux chiffres dans la partie fractionnaire sont arrondis
String format1 = String.format("%.2f", 1.2399);
System.out.println(format1); // "1,24"
// trois chiffres dans la partie fractionnaire sont arrondis
String format2 = String.format("%.3f", 1.2399);
System.out.println(format2); // "1,240"
Adhérence stricte à l’IEEE 754
Par défaut, les opérations flottantes sur float et double ne suivent pas strictement les règles de l’IEEE 754. Les extensions spécifiques à la plateforme sont autorisées, ce qui peut conduire à des résultats plus précis que nécessaires. La directive strictfp désactive ce comportement.
public strictfp class StrictCalculation {
public static double calculate(double a, double b) {
// Toutes les opérations dans cette classe
// suivent strictement IEEE 754
return (a * b) / (a + b);
}
}
Note importante : Depuis Java 17, toutes les opérations à virgule flottante sont strictement conformes à IEEE 754. Le mot-clé strictfp est devenu obsolète mais reste accepté pour la compatibilité.
Utiliser BigDecimal pour une précision exacte
Pour les calculs financiers ou tout contexte nécessitant une précision exacte, utilisez BigDecimal :
import java.math.BigDecimal;
import java.math.RoundingMode;
public class BigDecimalExample {
public static void main(String[] args) {
// INCORRECT : Créer BigDecimal depuis un double
BigDecimal bad = new BigDecimal(0.1);
System.out.println("Depuis double: " + bad);
// CORRECT : Créer BigDecimal depuis une String
BigDecimal good = new BigDecimal("0.1");
System.out.println("Depuis String: " + good);
// Calcul précis
BigDecimal a = new BigDecimal("0.1");
BigDecimal b = new BigDecimal("0.2");
BigDecimal sum = a.add(b);
System.out.println("0.1 + 0.2 = " + sum); // Affiche exactement 0.3
// Division avec arrondi
BigDecimal dividend = new BigDecimal("10");
BigDecimal divisor = new BigDecimal("3");
BigDecimal result = dividend.divide(divisor, 4, RoundingMode.HALF_UP);
System.out.println("10 / 3 = " + result); // 3.3333
}
}
Bonnes Pratiques
1. Choisir le bon type selon le contexte
public class TypeSelection {
// Pour les calculs scientifiques : double
public double calculatePhysics(double mass, double acceleration) {
return mass * acceleration;
}
// Pour les calculs financiers : BigDecimal
public BigDecimal calculatePrice(BigDecimal quantity, BigDecimal unitPrice) {
return quantity.multiply(unitPrice)
.setScale(2, RoundingMode.HALF_UP);
}
// Pour les coordonnées GPS : double (suffisant)
public double calculateDistance(double lat1, double lon1,
double lat2, double lon2) {
// Formule de Haversine
double R = 6371; // Rayon de la Terre en km
double dLat = Math.toRadians(lat2 - lat1);
double dLon = Math.toRadians(lon2 - lon1);
double a = Math.sin(dLat/2) * Math.sin(dLat/2) +
Math.cos(Math.toRadians(lat1)) *
Math.cos(Math.toRadians(lat2)) *
Math.sin(dLon/2) * Math.sin(dLon/2);
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
return R * c;
}
}
2. Toujours utiliser une comparaison avec epsilon
public class FloatComparator {
private static final double EPSILON = 1e-9;
public static boolean equals(double a, double b) {
return Math.abs(a - b) < EPSILON;
}
public static boolean equals(double a, double b, double epsilon) {
return Math.abs(a - b) < epsilon;
}
// Comparaison relative pour des valeurs de grande magnitude
public static boolean relativeEquals(double a, double b, double tolerance) {
double diff = Math.abs(a - b);
double largest = Math.max(Math.abs(a), Math.abs(b));
return diff <= largest * tolerance;
}
}
3. Éviter les accumulations d’erreurs
public class AccumulationExample {
public static void main(String[] args) {
// MAUVAIS : Accumulation d'erreurs
double badSum = 0.0;
for (int i = 0; i < 1000; i++) {
badSum += 0.1;
}
System.out.println("Somme naive: " + badSum); // 99.99999999999857
// BON : Utiliser Kahan summation algorithm
double sum = 0.0;
double c = 0.0; // Compensation pour les bits perdus
for (int i = 0; i < 1000; i++) {
double y = 0.1 - c;
double t = sum + y;
c = (t - sum) - y;
sum = t;
}
System.out.println("Somme Kahan: " + sum); // Plus proche de 100.0
}
}
Pieges Courants
Piege 1 : Comparer avec == ou !=
// INCORRECT
double a = 0.1 + 0.2;
if (a == 0.3) { // Ne sera JAMAIS vrai !
System.out.println("Égal");
}
// CORRECT
if (Math.abs(a - 0.3) < 1e-9) {
System.out.println("Égal");
}
Piege 2 : Créer BigDecimal depuis un double
// INCORRECT - conserve l'erreur de précision
BigDecimal wrong = new BigDecimal(0.1);
// wrong vaut 0.1000000000000000055511151231257827...
// CORRECT - précision parfaite
BigDecimal correct = new BigDecimal("0.1");
// correct vaut exactement 0.1
Piege 3 : Ignorer les valeurs spéciales
public class SpecialValues {
public static void main(String[] args) {
double inf = 1.0 / 0.0;
double negInf = -1.0 / 0.0;
double nan = 0.0 / 0.0;
System.out.println("1/0 = " + inf); // Infinity
System.out.println("-1/0 = " + negInf); // -Infinity
System.out.println("0/0 = " + nan); // NaN
// Attention : NaN n'est égal à rien, même pas à lui-même !
System.out.println("NaN == NaN ? " + (nan == nan)); // false
System.out.println("Double.isNaN(nan) ? " + Double.isNaN(nan)); // true
// Toujours vérifier avant les calculs
if (Double.isFinite(inf)) {
// Calcul sûr
}
}
}
Piege 4 : Utiliser float au lieu de double sans raison
// INCORRECT - perte de précision inutile
float x = 0.1f;
float y = 0.2f;
float z = x + y;
System.out.println(z); // 0.3 mais avec moins de précision
// CORRECT - utilisez double par défaut
double x2 = 0.1;
double y2 = 0.2;
double z2 = x2 + y2;
Piege 5 : Oublier l’arrondi dans les divisions BigDecimal
import java.math.BigDecimal;
import java.math.RoundingMode;
// INCORRECT - ArithmeticException si division non exacte
try {
BigDecimal result = new BigDecimal("10").divide(new BigDecimal("3"));
} catch (ArithmeticException e) {
System.out.println("Erreur : " + e.getMessage());
}
// CORRECT - Toujours spécifier l'échelle et le mode d'arrondi
BigDecimal result = new BigDecimal("10")
.divide(new BigDecimal("3"), 10, RoundingMode.HALF_UP);
System.out.println(result); // 3.3333333333
Classe utilitaire complète
Voici une classe utilitaire que vous pouvez utiliser dans vos projets :
import java.math.BigDecimal;
import java.math.RoundingMode;
public final class FloatUtils {
private static final double DEFAULT_EPSILON = 1e-9;
private FloatUtils() {}
public static boolean equals(double a, double b) {
return equals(a, b, DEFAULT_EPSILON);
}
public static boolean equals(double a, double b, double epsilon) {
if (Double.isNaN(a) || Double.isNaN(b)) {
return false;
}
if (Double.isInfinite(a) || Double.isInfinite(b)) {
return a == b;
}
return Math.abs(a - b) < epsilon;
}
public static int compare(double a, double b, double epsilon) {
if (equals(a, b, epsilon)) {
return 0;
}
return Double.compare(a, b);
}
public static BigDecimal toMoney(double value) {
return BigDecimal.valueOf(value)
.setScale(2, RoundingMode.HALF_UP);
}
public static boolean isZero(double value) {
return equals(value, 0.0);
}
}
Conclusion
Les problèmes liés aux valeurs flottantes dans Java peuvent sembler mineurs, mais ils ont des conséquences importantes sur les résultats de votre application. Voici les points clés à retenir :
-
Ne jamais comparer des flottants avec
==: Utilisez toujours une comparaison avec epsilon ouDouble.compare(). -
Utilisez BigDecimal pour les calculs financiers : C’est la seule façon de garantir une précision exacte.
-
Préférez double à float : Sauf contrainte mémoire, double offre une meilleure précision.
-
Gérez les valeurs spéciales : Vérifiez toujours
NaNetInfinityavant les calculs critiques. -
Documentez votre choix de delta : Expliquez pourquoi vous avez choisi une valeur epsilon spécifique.
En appliquant ces bonnes pratiques, vous éviterez les bugs subtils liés à la précision numérique et produirez un code plus robuste et fiable.
Prochaines étapes
- Consultez la documentation officielle de
java.math.BigDecimalpour maîtriser ses nombreuses options. - Refactorisez vos comparaisons de flottants existantes avec une méthode epsilon appropriée.
- Identifiez les calculs financiers dans votre code et migrez-les vers BigDecimal.
- Ajoutez des tests unitaires spécifiques pour les cas limites (valeurs très grandes, très petites, NaN, Infinity).
In-Article Ad
Dev Mode
Tags
Mahmoud DEVO
Senior Full-Stack Developer
I'm a passionate full-stack developer with 10+ years of experience building scalable web applications. I write about Vue.js, Node.js, PostgreSQL, and modern DevOps practices.
Enjoyed this article?
Subscribe to get more tech content delivered to your inbox.
Related Articles
Manipuler les classes Java avec ASM et Javassist : bytecode, instrumentation et fichiers JAR
Apprenez a manipuler les classes Java avec ASM et Javassist : chargement, modification du bytecode, instrumentation et creation de fichiers JAR.
Synchronisation Java avec AtomicInteger : eviter la contention et optimiser les performances
Decouvrez comment utiliser les types atomiques Java (AtomicInteger, AtomicLong, AtomicReference, AtomicBoolean) pour reduire la contention, eviter les blocages et ameliorer les performances.
Les avantages des flux tampons pour une performance optimale
Ameliorez les performances de votre code Java avec les flux tampons ! Decouvrez comment reduire considerablement le nombre d'appels systeme, optimiser l'utilisation des types primitifs, gerer efficacement la journalisation et iterer sur les Maps de maniere performante.