Développons en Java
v 2.40   Copyright (C) 1999-2023 .   
7. Les packages de bases 9. La gestion des exceptions Imprimer Index Index avec sommaire Télécharger le PDF

 

8. Les fonctions mathématiques

 

chapitre    8

 

Niveau : niveau 2 Elémentaire 

 

La classe java.lang.Math contient une série de méthodes et variables mathématiques. Comme la classe Math fait partie du package java.lang, elle est automatiquement importée. De plus, il n'est pas nécessaire de déclarer un objet de type Math car les méthodes sont toutes static.

Exemple ( code Java 1.1 ) : Calculer et afficher la racine carrée de 3
public class Math1 {
  public static void main(java.lang.String[] args) {
    System.out.println(" = " + Math.sqrt(3.0));
  }
}

Ce chapitre contient plusieurs sections :

 

8.1. Les variables de classe

PI représente pi dans le type double ( 3,14159265358979323846 )

E représente e dans le type double ( 2,7182818284590452354 )

Exemple ( code Java 1.1 ) :
public class Math2 {
  public static void main(java.lang.String[] args) {
    System.out.println(" PI = "+Math.PI);
    System.out.println(" E = "+Math.E);
  }
}

 

8.2. Les fonctions trigonométriques

Les méthodes sin(), cos(), tan(), asin(), acos(), atan() sont déclarées : public static double fonctiontrigo(double angle)

Les angles doivent être exprimés en radians. Pour convertir des degrés en radian, il suffit de les multiplier par PI/180

 

8.3. Les fonctions de comparaisons

max (n1, n2)
min (n1, n2)

Ces méthodes existent pour les types int, long, float et double : elles déterminent respectivement les valeurs maximales et minimales des deux paramètres.

Exemple ( code Java 1.1 ) :
public class Math1 {

  public static void main(String[] args) {
    System.out.println(" le plus grand = " + Math.max(5, 10));
    System.out.println(" le plus petit = " + Math.min(7, 14));
  }
}

Résultat :
le plus grand = 10
le plus petit = 7

 

8.4. Les arrondis

La classe Math propose plusieurs méthodes pour réaliser différents arrondis.

 

8.4.1. La méthode round(n)

Pour les types float et double, cette méthode ajoute 0,5 à l'argument et restitue la plus grande valeur entière (int) inférieure ou égale au résultat.

Exemple ( code Java 1.1 ) :
public class Arrondis1 {
  static double[] valeur = {-5.7, -5.5, -5.2, -5.0, 5.0, 5.2, 5.5, 5.7 };

  public static void main(String[] args) {
    for (int i = 0; i < valeur.length; i++) {
      System.out.println("round("+valeur[i]+") = "+Math.round(valeur[i]));
    }
  }
}

Résultat :
round(-5.7) = -6
round(-5.5) = -5
round(-5.2) = -5
round(-5.0) = -5
round(5.0) = 5
round(5.2) = 5
round(5.5) = 6
round(5.7) = 6

 

8.4.2. La méthode rint(double)

Cette méthode effectue la même opération mais renvoie un type double.

Exemple ( code Java 1.1 ) :
public class Arrondis2 {
  static double[] valeur = {-5.7, -5.5, -5.2, -5.0, 5.0, 5.2, 5.5, 5.7 };

  public static void main(String[] args) {
    for (int i = 0; i < valeur.length; i++) {
    System.out.println("rint("+valeur[i]+") = "+Math.rint(valeur[i]));
    }
  }
}

Résultat :
rint(-5.7) = -6.0
rint(-5.5) = -6.0
rint(-5.2) = -5.0
rint(-5.0) = -5.0
rint(5.0) = 5.0
rint(5.2) = 5.0
rint(5.5) = 6.0
rint(5.7) = 6.0

 

8.4.3. La méthode floor(double)

Cette méthode renvoie l'entier le plus proche inférieur ou égal à l'argument.

Exemple ( code Java 1.1 ) :
public class Arrondis3 {
  static double[] valeur = {-5.7, -5.5, -5.2, -5.0, 5.0, 5.2, 5.5, 5.7 };

  public static void main(String[] args) {
    for (int i = 0; i < valeur.length; i++) {
      System.out.println("floor("+valeur[i]+") = "+Math.floor(valeur[i]));
    }
  }
}

Résultat :
floor(-5.7) = -6.0
floor(-5.5) = -6.0
floor(-5.2) = -6.0
floor(-5.0) = -5.0
floor(5.0) = 5.0
floor(5.2) = 5.0
floor(5.5) = 5.0
floor(5.7) = 5.0

 

8.4.4. La méthode ceil(double)

Cette méthode renvoie l'entier le plus proche supérieur ou égal à l'argument

Exemple ( code Java 1.1 ) :
public class Arrondis4 {
  static double[] valeur = {-5.7, -5.5, -5.2, -5.0, 5.0, 5.2, 5.5, 5.7 };

  public static void main(String[] args) {
    for (int i = 0; i < valeur.length; i++) {
      System.out.println("ceil("+valeur[i]+") = "+Math.ceil(valeur[i]));
    }
  }
}

Résultat :
ceil(-5.7) = -5.0
ceil(-5.5) = -5.0
ceil(-5.2) = -5.0
ceil(-5.0) = -5.0
ceil(5.0) = 5.0
ceil(5.2) = 6.0
ceil(5.5) = 6.0
ceil(5.7) = 6.0

 

8.4.5. La méthode abs(x)

Cette méthode donne la valeur absolue de x (les nombres négatifs sont convertis en leur opposé). La méthode est définie pour les types int, long, float et double.

Exemple ( code Java 1.1 ) :
public class Math1 {
  public static void main(String[] args) {
    System.out.println(" abs(-5.7) = "+Math.abs(-5.7));
  }
}

Résultat :
abs(-5.7) = 5.7

 

8.5. La méthode IEEEremainder(double, double)

Cette méthode renvoie le reste de la division du premier argument par le deuxième

Exemple ( code Java 1.1 ) :
public class Math1 {
  public static void main(String[] args) {
    System.out.println(" reste de la division de 10 par 3 = "
      +Math.IEEEremainder(10.0, 3.0) );
  }
}

Résultat :
reste de la division de 10 par 3 = 1.0

 

8.6. Les Exponentielles et puissances

 

8.6.1. La méthode pow(double, double)

Cette méthode élève le premier argument à la puissance indiquée par le second.

Exemple ( code Java 1.1 ) :
public static void main(java.lang.String[] args) {
  System.out.println(" 5 au cube  = "+Math.pow(5.0, 3.0) );
}

Résultat :
5 au cube  = 125.0

 

8.6.2. La méthode sqrt(double)

Cette méthode calcule la racine carrée de son paramètre.

Exemple ( code Java 1.1 ) :
public static void main(java.lang.String[] args) {
  System.out.println(" racine carrée de 25  = "+Math.sqrt(25.0) );
}

Résultat :
racine carrée de 25 = 5.0

 

8.6.3. La méthode exp(double)

Cette méthode calcule l'exponentielle de l'argument

Exemple ( code Java 1.1 ) :
public static void main(java.lang.String[] args) {
  System.out.println(" exponentiel de 5  = "+Math.exp(5.0) );
}

Résultat :
exponentiel de 5  = 148.4131591025766

 

8.6.4. La méthode log(double)

Cette méthode calcule le logarithme naturel de l'argument

Exemple ( code Java 1.1 ) :
public static void main(java.lang.String[] args) {
  System.out.println(" logarithme de 5  = "+Math.log(5.0) );
}

Résultat :
logarithme de 5  = 1.6094379124341003

 

8.7. La génération de nombres aléatoires

La méthode random() renvoie un nombre aléatoire compris entre 0.0 et 1.0.

Exemple ( code Java 1.1 ) :
public static void main(java.lang.String[] args) {
  System.out.println(" un nombre aléatoire  = "+Math.random() );
}

Résultat :
un nombre aléatoire  = 0.8178819778125899

 

8.8. La classe BigDecimal

La classe java.math.BigDecimal est incluse dans l'API Java depuis la version 5.0.

La classe BigDecimal qui hérite de la classe java.lang.Number permet de réaliser des calculs en virgule flottante avec une précision dans les résultats similaire à celle de l'arithmétique scolaire.

La classe BigDecimal permet ainsi une représentation exacte des valeurs ce que ne peuvent garantir les données primitives de type numérique flottant (float ou double). Les calculs en virgule flottante privilégient en effet la vitesse de calcul plutôt que la précision.

Exemple :
package fr.jmdoudoux.dej.bigdecimal;

public class CalculDouble {

  public static void main(String[] args) {
    double valeur = 10*0.09;
    System.out.println(valeur);
  }
}

Résultat :
0.8999999999999999

Cependant certains calculs, notamment ceux relatifs à des aspects financiers par exemple, requièrent une précision particulière : ces calculs utilisent généralement une précision de deux chiffres.

La classe BigDecimal permet de réaliser de tels calculs en permettant d'avoir le contrôle sur la précision (nombre de décimales significatives après la virgule) et la façon dont l'arrondi est réalisé.

Exemple :
package fr.jmdoudoux.dej.bigdecimal;

import java.math.BigDecimal;

public class CalculBigDecimal {

  public static void main(
      String[] args) {
    BigDecimal valeur1 = new BigDecimal("10");
    BigDecimal valeur2 = new BigDecimal("0.09");

    BigDecimal valeur = valeur1.multiply(valeur2);
    
    System.out.println(valeur);
  }
}

Résultat :
0.90

De plus, la classe BigDecimal peut gérer des valeurs possédant plus de 16 chiffres significatifs après la virgule.

La classe BigDecimal propose de nombreux constructeurs qui attendent en paramètre la valeur en différents types.

Remarque : il est préférable d'utiliser le constructeur attendant en paramètre la valeur sous forme de chaîne de caractères.

Exemple :
package fr.jmdoudoux.dej.bigdecimal;

import java.math.BigDecimal;

public class CalculBigDecimal3 {

  public static void main(String[] args) {
    BigDecimal valeur1 = new BigDecimal(2.8);
    BigDecimal valeur2 = new BigDecimal("2.8");
    
    System.out.println("valeur1="+valeur1);
    System.out.println("valeur2="+valeur2); 
  }
}

Résultat :
valeur1=2.79999999999999982236431605997495353221893310546875
valeur2=2.8

Avec cette classe, il est parfois nécessaire de devoir créer une nouvelle instance de BigDecimal à partir de la valeur d'une autre instance de BigDecimal. Aucun constructeur de la classe BigDecimal n'attend en paramètre un objet de type BigDecimal : il est nécessaire d'utiliser le constructeur qui attend en paramètre la valeur sous la forme d'une chaîne de caractères et de lui passer en paramètre le résultat de l'appel de la méthode toString() de l'instance de BigDecimal encapsulant la valeur.

La classe BigDecimal propose de nombreuses méthodes pour réaliser des opérations arithmétiques sur la valeur qu'elle encapsule telles que add(), substract(), multiply(), divide(), min(), max(), pow(), remainder(), divideToIntegralValue(), ...

Le classe BigDecimal est immuable : la valeur qu'elle encapsule ne peut pas être modifiée. Toutes les méthodes qui effectuent une opération sur la valeur encapsulée retournent un nouvel objet de type BigDecimal qui encapsule le résultat de l'opération.

Une erreur courante est d'invoquer la méthode mais de ne pas exploiter le résultat de son exécution.

Exemple :
package fr.jmdoudoux.dej.bigdecimal;

import java.math.BigDecimal;

public class CalculBigDecimal7 {
  public static void main(String[] args) {
    BigDecimal valeur = new BigDecimal("10.5");
    BigDecimal bonus = new BigDecimal("4.2");
    
    valeur.add(bonus);
    System.out.println("valeur=" + valeur);
    
    valeur = valeur.add(bonus);
    System.out.println("valeur=" + valeur);
  }
}

Résultat :
valeur=10.5
valeur=14.7

La méthode setScale() permet de spécifier la précision de la valeur et éventuellement le mode d'arrondi à appliquer. Elle retourne un objet de type BigDecimal correspondant aux caractéristiques fournies puisque l'objet BigDecimal est immuable.

C'est une bonne pratique de toujours préciser le mode d'arrondi car si un arrondi est nécessaire et que le mode d'arrondi n'est pas précisé alors une exception de type ArithmeticException est levée.

Exemple :
package fr.jmdoudoux.dej.bigdecimal;

import java.math.BigDecimal;

public class CalculBigDecimal4 {

  public static void main(String[] args) {
    BigDecimal valeur1 = new BigDecimal(2.8);
    valeur1.setScale(1);
    System.out.println("valeur1="+valeur1);
  }
}

Résultat :
Exception in thread "main" java.lang.ArithmeticException: Rounding necessary
        at java.math.BigDecimal.divide(BigDecimal.java:1346)
        at java.math.BigDecimal.setScale(BigDecimal.java:2310)
        at java.math.BigDecimal.setScale(BigDecimal.java:2350)
        at fr.jmdoudoux.dej.bigdecimal.CalculBigDecimal4.main(CalculBigDecimal4.java:10)

La classe BigDecimal propose plusieurs modes d'arrondis : ROUND_CEILING, ROUND_DOWN, ROUND_FLOOR, ROUND_HALF_UP, ROUND_HALF_DOWN, ROUND_HALF_EVEN, ROUND_UNNECESSARY et ROUND_UP

Exemple :
package fr.jmdoudoux.dej.bigdecimal;

import java.math.BigDecimal;

public class CalculBigDecimal5 {

  public static void main(String[] args) {
    BigDecimal valeur = null;
    String strValeur = null;

    strValeur = "0.222";
    valeur = (new BigDecimal(strValeur)).setScale(2, BigDecimal.ROUND_CEILING);
    System.out.println("ROUND_CEILING    "+strValeur+" :  "+valeur.toString());    
    
    strValeur = "-0.222";
    valeur = (new BigDecimal(strValeur)).setScale(2, BigDecimal.ROUND_CEILING);
    System.out.println("ROUND_CEILING   "+strValeur+" : "+valeur.toString());    

    strValeur = "0.222";
    valeur = (new BigDecimal(strValeur)).setScale(2, BigDecimal.ROUND_DOWN);
    System.out.println("ROUND_DOWN       "+strValeur+" :  "+valeur.toString());    

    strValeur = "0.228";
    valeur = (new BigDecimal(strValeur)).setScale(2, BigDecimal.ROUND_DOWN);
    System.out.println("ROUND_DOWN       "+strValeur+" :  "+valeur.toString());    

    strValeur = "-0.228";
    valeur = (new BigDecimal(strValeur)).setScale(2, BigDecimal.ROUND_DOWN);
    System.out.println("ROUND_DOWN      "+strValeur+" : "+valeur.toString());    

    strValeur = "0.222";
    valeur = (new BigDecimal(strValeur)).setScale(2, BigDecimal.ROUND_FLOOR);
    System.out.println("ROUND_FLOOR      "+strValeur+" :  "+valeur.toString());    

    strValeur = "-0.222";
    valeur = (new BigDecimal(strValeur)).setScale(2, BigDecimal.ROUND_FLOOR);
    System.out.println("ROUND_FLOOR     "+strValeur+" : "+valeur.toString());    

    strValeur = "0.222";
    valeur = (new BigDecimal(strValeur)).setScale(2, BigDecimal.ROUND_HALF_UP);
    System.out.println("ROUND_HALF_UP    "+strValeur+" :  "+valeur.toString());    
    
    strValeur = "0.225";
    valeur = (new BigDecimal(strValeur)).setScale(2, BigDecimal.ROUND_HALF_UP);
    System.out.println("ROUND_HALF_UP    "+strValeur+" :  "+valeur.toString());    

    strValeur = "0.225";
    valeur = (new BigDecimal(strValeur)).setScale(2, BigDecimal.ROUND_HALF_DOWN);
    System.out.println("ROUND_HALF_DOWN  "+strValeur+" :  "+valeur.toString());    
    
    strValeur = "0.226";
    valeur = (new BigDecimal(strValeur)).setScale(2, BigDecimal.ROUND_HALF_DOWN);
    System.out.println("ROUND_HALF_DOWN  "+strValeur+" :  "+valeur.toString());    

    strValeur = "0.215";
    valeur = (new BigDecimal(strValeur)).setScale(2, BigDecimal.ROUND_HALF_EVEN);
    System.out.println("ROUND_HALF_EVEN  "+strValeur+" :  "+valeur.toString());    
    
    strValeur = "0.225";

    valeur = (new BigDecimal(strValeur)).setScale(2, BigDecimal.ROUND_HALF_EVEN);
    System.out.println("ROUND_HALF_EVEN  "+strValeur+" :  "+valeur.toString());    

    strValeur = "0.222";
    valeur = (new BigDecimal(strValeur)).setScale(2, BigDecimal.ROUND_UP);
    System.out.println("ROUND_UP         "+strValeur+" :  "+valeur.toString());    
    
    strValeur = "0.226";
    valeur = (new BigDecimal(strValeur)).setScale(2, BigDecimal.ROUND_UP);
    System.out.println("ROUND_UP         "+strValeur+" :  "+valeur.toString());    
  }
}

Résultat :
ROUND_CEILING    0.222 :  0.23
ROUND_CEILING   -0.222 : -0.22
ROUND_DOWN       0.222 :  0.22
ROUND_DOWN       0.228 :  0.22
ROUND_DOWN      -0.228 : -0.22
ROUND_FLOOR      0.222 :  0.22
ROUND_FLOOR     -0.222 : -0.23
ROUND_HALF_UP    0.222 :  0.22
ROUND_HALF_UP    0.225 :  0.23
ROUND_HALF_DOWN  0.225 :  0.22
ROUND_HALF_DOWN  0.226 :  0.23
ROUND_HALF_EVEN  0.215 :  0.22
ROUND_HALF_EVEN  0.225 :  0.22
ROUND_UP         0.222 :  0.23
ROUND_UP         0.226 :  0.23

Le mode d'arrondi doit aussi être précisé lors de l'utilisation de la méthode divide().

Exemple :
package fr.jmdoudoux.dej.bigdecimal;

import java.math.BigDecimal;

public class CalculBigDecimal6 {
  public static void main(String[] args) {
    BigDecimal valeur = new BigDecimal("1");
    System.out.println(valeur.divide(new BigDecimal("3")));
  }
}

Résultat :
Exception in thread "main" java.lang.ArithmeticException: 
Non-terminating decimal expansion; no exact representable decimal result.
        at java.math.BigDecimal.divide(BigDecimal.java:1514)
        at fr.jmdoudoux.dej.bigdecimal.CalculBigDecimal6.main(CalculBigDecimal6.java:9)

Le même exemple en précisant le mode d'arrondi fonctionne parfaitement.

Exemple :
package fr.jmdoudoux.dej.bigdecimal;

import java.math.BigDecimal;

public class CalculBigDecimal6 {
  public static void main(String[] args) {
    BigDecimal valeur = new BigDecimal("1");
    System.out.println(valeur.divide(new BigDecimal("3"),4,BigDecimal.ROUND_HALF_DOWN));
  }
}

Résultat :
0.3333

La précision et le mode d'arrondi doivent être choisis avec attention parce que leur choix peut avoir de grandes conséquences sur les résultats de calculs notamment si le résultat final est constitué de multiples opérations. Dans ce cas, il est préférable de garder la plus grande précision durant les calculs et de n'effectuer l'arrondi qu'à la fin.

Il faut être vigilent lors de la comparaison entre deux objets de type BigDecimal. La méthode equals() compare les valeurs mais en tenant compte de la précision. Ainsi, il est préférable d'utiliser la méthode compareTo() qui n'effectue la comparaison que sur la valeur.

Exemple :
package fr.jmdoudoux.dej.bigdecimal;

import java.math.BigDecimal;

public class CalculBigDecimal8 {

  public static void main(String[] args) {
    BigDecimal valeur1 = new BigDecimal("10.00");
    BigDecimal valeur2 = new BigDecimal("10.0");
    
    System.out.println("valeur1.equals(valeur2) = "+valeur1.equals(valeur2));
    System.out.println("valeur1.compareTo(valeur2) = "+(valeur1.compareTo(valeur2)==0)); 
  }
}

Résultat :
valeur1.equals(valeur2) = false
valeur1.compareTo(valeur2) = true

La méthode compareTo() renvoie 0 si les deux valeurs sont égales, renvoie -1 si la valeur de l'objet fourni en paramètre est plus petite et renvoie 1 si la valeur de l'objet fourni en paramètre est plus grande.

Il est possible de passer en paramètre de la méthode format() de la classe NumberFormat un objet de type BigDecimal : attention dans ce cas, le nombre de décimales est limité à 16.

Exemple formatage d'un BigDecimal avec un format monétaire :
package fr.jmdoudoux.dej.bigdecimal;

import java.math.*;
import java.text.*;
import java.util.*;

public class CalculBigDecimal9 {

  public static void main(String[] args) {
    BigDecimal payment = new BigDecimal("1234.567");
    NumberFormat n = NumberFormat.getCurrencyInstance(Locale.FRANCE);
    String s = n.format(payment);
    System.out.println(s);
  }
}

Résultat :
1 234,57 €

La mise en oeuvre de la classe BigDecimal est plutôt fastidieuse comparée à d'autres langages qui proposent un support natif d'un type de données décimal mais elle permet d'effectuer des calculs précis.

L'utilisation de la classe BigDecimal n'est recommandée que si une précision particulière est nécessaire car sa mise en oeuvre est coûteuse.

 

8.9. La précision des calculs en virgule flottante

L'un des principaux aspects du langage Java est l'indépendance vis-à-vis de la plate-forme : compiler et exécuter le même code sur différentes machines et s'assurer que le résultat est le même quel que soit le hardware et le système d'exploitation utilisés.

Ce but n'est pas complétement atteint notamment à cause de la précision des calculs en virgule flottante. Certaines architectures hardware ont été conçues pour être efficaces, tandis que d'autres ont été conçues pour être précises.

Ainsi, les machines les plus précises utilisent une taille de virgule flottante de 80 bits, tandis que les machines les plus efficaces/rapides utilisent des doubles de 64 bits. Cette différence de précision peut induire des résultats de calculs différents d'une même opération exécutée dans des JVM sur différentes plateformes.

La précision standard pour les calculs en virgule flottante peut varier selon la CPU utilisée : la précision de 32 bits est différente de celle de 64 bits pour les machines x86 par exemple.

Historiquement, par défaut, les calculs en virgule flottante avec Java sont dépendants de la plate-forme. Ainsi, la précision du résultat du calcul en virgule flottante dépend du matériel utilisé et peut donc varier selon l'environnement d'exécution. Lors de l'exécution de calculs en virgules flottantes sur différentes plates-formes, les résultats peuvent donc varier en raison de la capacité de la CPU à traiter les virgules flottantes.

 

8.9.1. Les calculs en virgule flottante stricte

Différentes plates-formes utilisent un hardware différent qui calcule en virgule flottante avec plus de précision et une plus grande plage de valeurs que ne l'exige la spécification Java. Cela peut produire des résultats différents sur différentes plates-formes.

Le standard IEEE 754 (Standard for Binary Floating-Point Arithmetic) définit une norme standard pour les calculs en virgule flottante et le stockage des valeurs en virgule flottante dans différents formats, y compris la précision simple (32 bits, utilisée par le type float) ou double (64 bits, utilisée par le type double). Elle définit également des normes pour les calculs intermédiaires et pour les formats de précision étendue.

Certains matériels peuvent fournir une précision plus élevée et/ou une plage d'exposants plus large. Sur ces architectures, il peut être plus efficace de calculer les résultats intermédiaires en utilisant ces formats étendus. Cela permet d'éviter les erreurs d'arrondi, les débordements et les sous-débordements qui se produiraient autrement, mais les programmes peuvent produire des résultats différents sur ces architectures.

Il était coûteux d'éviter l'utilisation de la précision étendue sur les machines x86 dotées de l'architecture traditionnelle à virgule flottante x87. Bien qu'il soit facile de contrôler la précision des calculs, la limitation de la plage d'exposants pour les résultats intermédiaires a nécessité des instructions supplémentaires coûteuses.

Avant la version 1.2 de la JVM, les calculs en virgule flottante devaient être stricts : tous les résultats intermédiaires en virgule flottante devaient se comporter comme s'ils étaient représentés à l'aide des précisions simples ou doubles de l'IEEE. Il était donc coûteux, sur le matériel courant basé sur x87, de s'assurer que les débordements se produisaient là où c'était nécessaire.

Depuis la version 1.2 de la JVM, les calculs intermédiaires sont, par défaut, autorisés à dépasser les plages d'exposants standard associées aux formats IEEE 32 bits et 64 bits. Ils peuvent être représentés comme un membre de l'ensemble de valeurs "extended-exponent". Sur des plates-formes telles que x87, les débordements et les sous-débordements peuvent ne pas se produire là où ils sont attendus, produisant des résultats peut-être plus précis, mais moins reproductibles.

Ainsi historiquement par défaut de Java 1.2 à Java 16, les calculs sur des nombres flottants (float ou double) sont réalisés de manière non stricte.

A partir de Java 17, les calculs sur des nombres flottants (float ou double) sont systématiquement réalisés de manière stricte. Le mot clé strictfp n'a alors plus d'utilité.

 

8.9.2. Le mot clé réservé strictfp

Les calculs en virgule flottante dépendent de la plate-forme : des résultats différents peuvent être obtenus lorsque du bytecode est exécuté sur différents processeurs. Pour résoudre ce type de problème, le mot-clé strictfp a été introduit dans la version 1.2 du JDK.

Le mot-clé réservé strictfp est utilisé pour garantir que les opérations en virgule flottante donnent le même résultat quel que soit la plate-forme utilisée pour les exécuter et donc d'être indépendant de l'architecture matérielle.

Le mot clé réservé strictfp s'utilise comme un modificateur dont le rôle est de demander d'appliquer la sémantique FP-Strict (Floating Point Strict) de la norme standard IEEE 754 lors des calculs en virgule flottante afin de garantir la portabilité sur toutes les plates-formes.

En l'absence d'overflow ou d'underflow, il n'y a pas de différence entre les résultats obtenus avec ou sans strictfp. Si la répétabilité est essentielle, le modificateur strictfp peut être utilisé pour s'assurer que les d'overflows et les underflows se produisent aux mêmes endroits sur toutes les plates-formes. Sans le modificateur strictfp, les résultats intermédiaires peuvent utiliser une plage d'exposants plus importante.

Le modificateur strictfp permet d'atteindre ses objectifs en représentant toutes les valeurs intermédiaires comme des valeurs IEEE avec précision simple ou de double, comme c'était le cas dans les versions antérieures à la JVM 1.2.

L'utilisation du modificateur strictfp permet de garantir que les résultats des calculs sur des valeurs flottantes soient les mêmes sur toutes les JVM. Si strictfp n'est pas utilisé, la JVM est libre d'utiliser toute précision supplémentaire disponible sur le matériel de la plate-forme d'exécution.

Le mot clé strictfp s'utilise comme un modificateur et ne peut être appliqué que sur :

L'élément déclaré avec le modificateur strictfp est dit FP-strict.

Toutes les méthodes déclarées dans une classe ou une interface, ainsi que tous les types imbriqués déclarés dans une classe sont implicitement strictfp si la classe ou l'interface est déclarée avec le modificateur strictfp.

Dans une expression FP-strict, toutes les valeurs intermédiaires en virgule flottante doivent être conformes à la spécification IEEE 754.

Dans une expression qui n'est pas FP-strict, une certaine marge de manoeuvre est accordée à une implémentation d'une JVM pour utiliser une plage d'exposants étendue pour représenter les résultats intermédiaires.

En particulier, les processeurs x86 peuvent stocker des résultats intermédiaires avec une précision différente de la spécification IEEE 754. La situation se complique lorsque le JIT optimise un calcul particulier, notamment en changeant l'ordre des instructions, ce qui peut entraîner un arrondi légèrement différent.

Si le modificateur strictfp n'est pas utilisé, alors la JVM et le compilateur JIT sont libres d'exécuter les calculs en virgule flottante comme ils le veulent. Pour des raisons de performance, ils délégueront probablement le calcul au processeur. Si strictfp est utilisé, les calculs doivent être conformes à norme IEEE 754, ce qui, dans la pratique, signifie probablement que la JVM effectuera elle-même les calculs.

Avec ce modificateur, les résultats des calculs en virgule flottante sont prédictibles et identiques, quelle que soit la plateforme sous-jacente sur laquelle s'exécute la JVM. L'inconvénient est que si la plateforme sous-jacente est capable de supporter une plus grande précision, des calculs strictfp ne pourront pas en profiter.

Le surcoût induit par strictfp dépend beaucoup du processeur et du JIT. Si le JIT peut générer des instructions SSE pour effectuer un calcul, la surcharge est faible voire nulle.

Les expressions constantes à la compilation utilisent toujours le comportement FP-strict.

Le modificateur strictfp est utile pour garantir d'obtenir les mêmes résultats dans les calculs flottants quel que soit la plateforme d'exécution notamment pour des tests unitaires reproductibles.

 

8.9.2.1. L'utilisation de strictfp sur des classes

Lors de l'utilisation du modificateur strictfp sur une classe, tous les calculs à l'intérieur de la classe utilisent des calculs en virgule flottante strictes selon la norme IEEE 754.

L'utilisation de strictfp sur une classe a pour effet de rendre toutes les expressions float ou double dans la déclaration de la classe (y compris dans les initialisateurs de variables, les initialisateurs d'instances, les initialisateurs statiques et les constructeurs) explicitement FP-strict.

Cela implique que toutes les méthodes déclarées dans la classe, et toutes les classes imbriquées et interfaces, records et énumérations déclarés dans la classe, sont implicitement strictfp.

Exemple ( code Java 1.2 ) :
strictfp class MaClasse {
   
    // toutes les methodes concrètes sont implicitement strictfp
}

Le modificateur strictfp peut être utilisé dans la définition d'une classe abstraite.

Exemple ( code Java 1.2 ) :
strictfp abstract class MaClasse {
   
    // toutes les methodes concrètes sont implicitement strictfp
}

Si une superclasse est déclarée avec le mot-clé strictfp, une sous-classe n'héritera pas de ce comportement.

 

8.9.2.2. L'utilisation de strictfp sur des méthodes non abstraites

Si une classe n'est pas déclarée avec strictfp, il est possible obtenir le comportement FP-strict méthode par méthode, en déclarant chaque méthode concernée avec le modificateur strictfp.

Lorsqu'il est utilisé sur une méthode, il fait en sorte que tous les calculs dans le corps de la méthode utilisent des calculs en virgule flottante strictes selon la norme IEEE 754.

Exemple ( code Java 1.2 ) :
class MaClasse {
  strictfp void calculer() {}
}

La présence ou l'absence du modificateur strictfp n'a absolument aucun effet sur les règles de surcharge des méthodes et d'implémentation des méthodes abstraites. Par exemple, il est permis à une méthode qui n'est pas FP-strict de surcharger une méthode FP-strict et il est permis à une méthode FP-strict de surcharger une méthode qui n'est pas FP-strict.

Le compilateur émet une erreur de compilation si une déclaration de méthode contenant le mot-clé abstract ou native contient également le mot clé strictfp.

Exemple ( code Java 1.2 ) :
class MaClasse {
  strictfp MaClasse() {}
}

Résultat :
C:\java>javac MaClasse.java
MaClasse.java:2: error: modifier native,strictfp not allowed here
  native strictfp MaClasse();
                  ^
1 error

 

8.9.2.3. L'utilisation de strictfp sur des interfaces

L'effet du modificateur strictfp est de faire en sorte que toutes les expressions flottantes ou doubles dans le corps d'une méthode par défaut ou statique soient explicitement FP-strict.

Cela implique que toutes les méthodes déclarées dans l'interface, et tous les types imbriqués déclarés dans l'interface, sont implicitement strictfp.

Exemple ( code Java 1.2 ) :
strictfp interface MonInterface {
  // toutes les méthodes sont implicitement strictes
}

Le compilateur émet une erreur si une déclaration de méthode d'une interface qui contient le mot-clé abstract (implicite ou explicite) contient également le mot-clé strictfp.

 

8.9.2.4. Les cas d'utilisation invalides de strictfp

Plusieurs cas d'usage de strictfp sont invalides : le mot-clé strictfp ne peut pas être utilisé dans la déclaration de méthodes abstraites, de variables ou de constructeurs.

Le mot-clé strictfp n'est pas utilisable dans la déclaration d'une méthode abstraite.

Exemple ( code Java 1.2 ) :
class MaClasse {  
 
  strictfp abstract void calculer();
}

Résultat :
C:\java>javac MaClasse.java
MaClasse.java:2: error: illegal combination of modifiers: abstract and strictfp
  strictfp abstract void calculer();
                         ^
MaClasse.java:1: error: MaClasse is not abstract and does not override abstract method
 calculer() in MaClasse
class MaClasse {
^
2 errors

Puisque les méthodes d'une interface sont implicitement abstraites, strictfp ne peut pas être utilisé sur les méthodes d'une interface qui ne sont pas static ou default.

Exemple ( code Java 1.2 ) :
strictfp interface MonInterface {
    
    strictfp double calculer(); 
}

Résultat :
C:\java>javac MonInterface.java
MonInterface.java:3: error: modifier strictfp not allowed here
    strictfp double calculer();
                    ^
1 error

Le mot-clé trictfp n'est pas utilisable dans la déclaration d'une variableou d'un champ.

Exemple ( code Java 1.2 ) :
class MaClasse {
  strictfp float valeur; // modifier strictfp not allowed here
}

Résultat :
C:\java>javac MaClasse.java
MaClasse.java:2: error: modifier strictfp not allowed here
  strictfp float valeur;
                 ^
1 error

Le mo-clé strictfp n'est pas utilisable dans la déclaration d'un constructeur. Contrairement aux méthodes, un constructeur ne peut pas avoir de modificateur strictfp : l'impossibilité de déclarer un constructeur comme strictfp, contrairement à une méthode, est un choix intentionnel de conception du langage qui garantit qu'un constructeur est FP-strict si et seulement si sa classe est FP-strict.

Exemple ( code Java 1.2 ) :
class MaClasse { 
  strictfp MaClasse() {}
}

Résultat :
C:\java>javac MaClasse.java
MaClasse.java:2: error: modifier strictfp not allowed here
  strictfp MaClasse() {}
           ^
1 error

 

8.9.2.5. L'utilisation de strictfp à partir de Java 17

La JEP 306, implémentée dans le JDK 17, rend les opérations en virgule flottante systématiquement strictes, plutôt que d'avoir à la fois une sémantique stricte de virgule flottante (strictfp) et une sémantique de virgule flottante par défaut potentiellement différente.

Les extensions SSE2 (Streaming SIMD Extensions 2), fournies dans les processeurs Pentium 4 et les processeurs ultérieurs à partir de 2001, peuvent prendre en charge les opérations strictes en virgule flottante de manière directe et sans overhead excessif.

Étant donné qu'Intel et AMD supportent depuis longtemps SSE2 et les extensions ultérieures qui permettent un support intégré de la sémantique flottante stricte, la motivation technique pour avoir une sémantique flottante par défaut différente de la sémantique stricte n'est plus nécessaire.

Comme le jeu d'instruction x87 n'est plus nécessaire sur les processeurs x86 supportant le SSE2 et que sur les processeurs modernes il n'y a plus de coûts supplémentaires en termes de performances, Java 17 a de nouveau rendu toutes les opérations en virgule flottante strictes, rétablissant ainsi la sémantique d'avant la version 1.2.

En Java 17, le modificateur strictfp est donc devenu inutile et ne doit plus être utilisé. Sa présence ou son absence n'a aucun effet sur les résultats des calculs, car toutes les opérations en virgule flottante sont strictes.

Le compilateur à partir du JDK 17 émet un avertissement à chaque utilisation du modificateur strictfp.

Résultat :
C:\java>java -version
openjdk version "17" 2021-09-14
OpenJDK Runtime Environment (build 17+35-2724)
OpenJDK 64-Bit Server VM (build 17+35-2724, mixed mode, sharing)
 
C:\java>javac StrictFP.java
StrictFP.java:3: warning: [strictfp] as of release 17, all floating-point expressions
 are evaluated strictly and 'strictfp' is not required
    public static strictfp double calculerStrict(double a, double b) {
                                  ^
1 warning

 


7. Les packages de bases 9. La gestion des exceptions Imprimer Index Index avec sommaire Télécharger le PDF    
Développons en Java
v 2.40   Copyright (C) 1999-2023 .