Dans le programme suivant, vous pouvez voir que chaque valeur légèrement inférieure à .5
est arrondie, sauf pour 0.5
.
for (int i = 10; i >= 0; i--) {
long l = Double.doubleToLongBits(i + 0.5);
double x;
do {
x = Double.longBitsToDouble(l);
System.out.println(x + " rounded is " + Math.round(x));
l--;
} while (Math.round(x) > i);
}
empreintes
10.5 rounded is 11
10.499999999999998 rounded is 10
9.5 rounded is 10
9.499999999999998 rounded is 9
8.5 rounded is 9
8.499999999999998 rounded is 8
7.5 rounded is 8
7.499999999999999 rounded is 7
6.5 rounded is 7
6.499999999999999 rounded is 6
5.5 rounded is 6
5.499999999999999 rounded is 5
4.5 rounded is 5
4.499999999999999 rounded is 4
3.5 rounded is 4
3.4999999999999996 rounded is 3
2.5 rounded is 3
2.4999999999999996 rounded is 2
1.5 rounded is 2
1.4999999999999998 rounded is 1
0.5 rounded is 1
0.49999999999999994 rounded is 1
0.4999999999999999 rounded is 0
J'utilise Java 6 mise à jour 31.
Résumé
Dans Java 6 (et probablement plus tôt), round(x)
est implémenté en tant que floor(x+0.5)
.1 C'est un bogue de spécification, précisément pour ce cas pathologique.2 Java 7 n'impose plus cette implémentation interrompue.3
Le problème
0.5 + 0.49999999999999994 est exactement 1 en double précision:
static void print(double d) {
System.out.printf("%016x\n", Double.doubleToLongBits(d));
}
public static void main(String args[]) {
double a = 0.5;
double b = 0.49999999999999994;
print(a); // 3fe0000000000000
print(b); // 3fdfffffffffffff
print(a+b); // 3ff0000000000000
print(1.0); // 3ff0000000000000
}
Cela s'explique par le fait que l'exposant 0.49999999999999994 est plus petit que 0.5, aussi, lorsqu'il est ajouté, sa mantisse est déplacée et le paramètre ULP devient plus grand.
La solution
Depuis Java 7, OpenJDK (par exemple) l'implémente ainsi:4
public static long round(double a) {
if (a != 0x1.fffffffffffffp-2) // greatest double value less than 0.5
return (long)floor(a + 0.5d);
else
return 0;
}
1. http://docs.Oracle.com/javase/6/docs/api/Java/lang/Math.html#round%28double%29
2. http://bugs.Java.com/bugdatabase/view_bug.do?bug_id=6430675 (remerciement de @SimonNickerson pour avoir trouvé cette information)
3. http://docs.Oracle.com/javase/7/docs/api/Java/lang/Math.html#round%28double%29
Cela semble être un bogue connu ( bogue Java 6430675: Math.round a un comportement surprenant pour 0x1.fffffffffffffp-2 ) qui a été corrigé dans Java 7.
Code source dans JDK 6:
public static long round(double a) {
return (long)Math.floor(a + 0.5d);
}
Code source dans JDK 7:
public static long round(double a) {
if (a != 0x1.fffffffffffffp-2) {
// a is not the greatest double value less than 0.5
return (long)Math.floor(a + 0.5d);
} else {
return 0;
}
}
Lorsque la valeur est 0.49999999999999994d, dans JDK 6, il appelle floor et renvoie donc 1, mais dans JDK 7, la condition if
vérifie si le nombre est la plus grande valeur double moins que 0,5 ou pas. Comme dans ce cas, le nombre n'est pas la plus grande valeur double inférieure à 0,5, le bloc else
renvoie donc 0.
Vous pouvez essayer 0.49999999999999999d, qui renverra 1, mais pas 0, car il s'agit de la plus grande valeur double inférieure à 0.5.
J'ai le même sur JDK 1.6 32 bits, mais sur Java 7 64 bits, j'ai 0 pour 0,49999999999999994, qui est arrondi à 0 et la dernière ligne n'est pas imprimée. Il semble s'agir d'un problème de VM. Toutefois, si vous utilisez des points flottants, vous devez vous attendre à ce que les résultats diffèrent quelque peu d'un environnement à l'autre (processeur, mode 32 ou 64 bits).
Et, lorsque vous utilisez round
ou inversez des matrices, etc., ces bits peuvent faire une énorme différence.
x64 sortie:
10.5 rounded is 11
10.499999999999998 rounded is 10
9.5 rounded is 10
9.499999999999998 rounded is 9
8.5 rounded is 9
8.499999999999998 rounded is 8
7.5 rounded is 8
7.499999999999999 rounded is 7
6.5 rounded is 7
6.499999999999999 rounded is 6
5.5 rounded is 6
5.499999999999999 rounded is 5
4.5 rounded is 5
4.499999999999999 rounded is 4
3.5 rounded is 4
3.4999999999999996 rounded is 3
2.5 rounded is 3
2.4999999999999996 rounded is 2
1.5 rounded is 2
1.4999999999999998 rounded is 1
0.5 rounded is 1
0.49999999999999994 rounded is 0
La réponse ci-après est un extrait de Oraclerapport de bogue 6430675 à. Visitez le rapport pour une explication complète.
Les méthodes {Math, StrictMath.round sont définies opérationnellement par
(long)Math.floor(a + 0.5d)
pour les doubles arguments. Bien que cette définition fonctionne habituellement comme prévu, elle donne le résultat surprenant de 1, plutôt que 0, pour 0x1.ffffffffffffffpp-2 (0.49999999999999994).
La valeur 0,49999999999999994 est la plus grande valeur en virgule flottante inférieure à 0,5. En tant que littéral hexadécimal à virgule flottante, sa valeur est 0x1.ffffffffffffffpp-2, ce qui est égal à (2 - 2 ^ 52) * 2 ^ -2. == (0,5 - 2 ^ 54). Par conséquent, la valeur exacte de la somme
(0.5 - 2^54) + 0.5
est 1 - 2 ^ 54. C'est à mi-chemin entre les deux nombres à virgule flottante adjacents (1 - 2 ^ 53) et 1. Dans le mode arithmétique IEEE 754 au mode d'arrondi égal le plus proche utilisé par Java, lorsqu'un résultat en virgule flottante est inexact, le plus proche des deux les valeurs en virgule flottante représentables qui encadrent le résultat exact doivent être renvoyées; si les deux valeurs sont également proches, celle dont le dernier bit zéro est renvoyé. Dans ce cas, la valeur renvoyée correcte par add est 1 et non la plus grande valeur inférieure à 1.
Alors que la méthode fonctionne telle que définie, le comportement sur cette entrée est très surprenant; la spécification pourrait être modifiée en quelque chose de plus semblable à "Arrondir au plus long, arrondir les nœuds", ce qui permettrait de modifier le comportement de cette entrée.