2. Calculs

D'une façon générale, lorsque l'on fait du calcul numérique sur ordinateur, on différencie le calcul sur les entiers du calcul sur les flottants (nombres à virgule). Les entiers et le flottants ne sont pas codés de la même façon en mémoire, et les algorithmes qui permettent de les additionner, ou de les multiplier ne sont pas du tout les mêmes. Naturellement, les standards qui encadrent ces algorithmes, et leurs effets de bord, sont différents aussi.

2.1. Précision

La précision d'un calcul est un terme qui désigne le nombre de bits sur lequel ce calcul est effectué pour les entiers, et sur le type pour les opérations en virgule flottante. Rappelons que les deux types entier définis en Java sont int et long. Les deux types flottants sont float et double. Java ne différe pas des autres langages sur ce point. Le principe est qu'une opération se déroule toujours en double ou simple précision pour les opérations flottantes, et en long ou int pour les opérations entières. Notamment, le résultat de l'addition de deux short en Java est un int. Voici le détail des règles de conversion :
  • si l'une des deux opérandes est un double alors le calcul est effectué en double ;
  • sinon, si l'une des deux opérandes est un float, alors le calcul est effectué en float ;
  • sinon, si l'une des deux opérandes est un long alors le calcul est effectué en long ;
  • toutes les autres opérations sont effectuées en int.
Par exemple, l'addition d'un float et d'un long commence par la conversion de ce long en float. L'addition est ensuite effectuée, et le résultat est un float.

2.2. Codage des nombres flottants

Un nombre flottant x peut toujours s'écrire de la manière suivante, où a est compris entre 1 et 10 :
 x = ± a.10^n
Dans cette écriture a est appelé mantisse et n exposant . Chacun de ces nombres est codé sur un certain nombre de bits, qui dépend du type float ou double. La façon de coder ces nombres est précisée par la norme IEEE 754, que suit Java. Le premier type correspond au « simple précision » et est codé sur 32 bits, le second, « double précision » est codé sur 64 bits. L’allocation des bits suit le tableau suivant.

Tableau 5. Codage des nombres en virgule flottante

Signe Exposant Mantisse
float 1 8 bits, signés 23 bits
double 1 11 bits, signés 52 bits

Les nombres flottants ont donc une précision limitée, ce qui peut générer des résultats bizarres. Voyons deux exemples.

Exemple 59. Imprécision des flottants - 1

float f = (float)0.0 ;  // 0.0 est un double, on le caste donc en float, 
                        // on aurait pu écrire aussi 0.0f ou 0.0F  
 for (int i =  0 ; i <  10 ; i++) {  
    f += (float)  0.1 ;
}
     
System.out.println("Valeur de f = " + f) ;

Le résultat n'est pas celui auquel on pourrait s'attendre :
 > Valeur de f = 1.0000001
On peut refaire le test, en remplaçant le type float par double. Le résultat est effectivement un peu plus précis.
 > Valeur de f = 0.9999999999999999
Voici un dernier exemple, cette fois-ci sur la conversion d'un entier en flottant.

Exemple 60. Imprécision sur les flottants - 2

long l =  1000000000000000000L ;  // 1e18 : 1 milliard de milliards !  
 float f = (float) l ;  
l = (long) f ;  
System.out.println("Valeur de l : " + l) ;

Cette fois le résultat est le suivant :
 > Valeur de l : 999999984306749440
Contrairement à l'exemple précédent, le passage de cette conversion vers la double précision conserve la valeur de l'entier, puisqu'il y a suffisament de bits pour coder cette valeur sans erreur. On voit sur ces deux exemples que la manipulation des nombres en virgules flottantes est parfois délicate. En particulier, tenter un test d'égalité entre deux nombres flottants mène le plus souvent à des problèmes. On préfèrera systématiquement tester la valeur absolue normalisée de la différence, comme dans l'exemple suivant.

Exemple 61. Tester l'égalité de deux flottants

 float f1 = grosCalcul(...) ;  // une valeur  
 float f2 = autreGrosCalul(...) ;  // une autre valeur  
  
 if (f1 == f2) {  
    victoire() ;  // ce code a toutes les chances de ne jamais être executé !
}  
  
 if (Math.abs(f1 - f2)/Math.max(f1, f2) <  1e-5) {  // 1e-5 règle la précision 
                                                     // de l'"égalité"  
    victoire() ;  
}

2.3. Le mot-clé strictfp

Depuis Java 2, un mot-clé a été introduit dans le domaine du calcul en virgule flottante : stricfp. Ce mot-clé s'utilise en tête de déclaration d'une méthode, et impose que tous les calculs effectués dans cette méthode seront menés en simple précision. Effectivement, la plupart des processeurs modernes dispose d'une FPU (floating point unit), chargée d'exécuter rapidement les calculs en virgule flottante. Ces FPU sont conçues pour opérer sur des double, et sont systématiquement appelées lorsque le processeur a besoin de faire une opération flottante, même quand il doit la faire sur des float. Un calcul déclaré en flottant est donc effectué en double précision, puis son résultat est converti en float. Or, tous les processeurs ne sont pas équipés de cette FPU. Deux calculs écrits de la même façon peuvent donc mener à des résultats légèrement différents suivant le processeur qui les prend en charge, et notamment suivant que ce calcul s'effectue sur une FPU avec conversion ou non. Ce point viole le principe de portabilité de Java. Le mot-clé stricfp a donc été introduit, et permet de parer ce problème en fixant le mode de calcul, au détriment éventuel de la performance.

Exemple 62. Utilisation de strictfp

strictfp  float monCalcul(...) {  // syntaxe d'utilisation de strifp  
    ... ;   
}

2.4. Conversion de types

Lorsque l'on écrit l'affectation d'une variable à une autre, ou d'une expression à une variable, une conversion peut avoir lieu. Deux cas peuvent alors se présenter : soit cette conversion s'effectue d'un nombre d'une précision donnée vers une précision meilleure, soit l'inverse. Le premier cas ne pose de problème ni pour les entiers, ni pour les flottants. La conversion est faite silencieusement, sans déclaration particulière. Il n'y a pas de perte de précision lors de ces conversions. Le second cas lui, pose problème, car il peut mener à une perte de précision. Java l'interdit donc et génère une erreur de compilation ; il faut utiliser un cast explicite pour qu'elle se fasse.

Exemple 63. Conversion d'un long en int

long j =  0 ;  
 int i =  0 ;  
  
j = i ;  // conversion par défaut d'un int en long  
i = j ;  // ERREUR DE COMPILATION !!!!!  
i = (int)j ;  // syntaxe correcte

Les conversions peuvent survenir en des endroits un peu plus subtils. Il faut se rappeler que les opérations entre types entiers en Java se font soit en int soit en long, même si les deux opérandes sont de même type. Si l'on fait des opérations sur des byte par exemple, des conversions de type peuvent être nécessaires.

Exemple 64. Addition d'un int à un byte

byte b =  0 ;  
b +=  1 ;  // code correct  
b = b +  1 ;  // NE COMPILE PAS !!! car b + 1 est de type int   
b = b + (byte)1 ;  // NE COMPILE PAS PLUS !!! car b + 1 est toujours un int  
b = (byte)(b +  1) ;  // code correct

La perte de précision sur la ligne 3 est due au fait que l’opération + a généré la conversion en int des deux opérandes. On s’en rend compte puisque le fait de forcer l’utilisation d’un byte pour le 1 ne change rien. Le résultat est donc un int, que l’on veut affecter à un byte. L’utilisation d’une conversion explicite permet de lever l’alerte.
Java langage & API
Retour au blog Java le soir
Cours & Tutoriaux
Table des matières
Introduction : un peu d'histoire
1. Java : genèse d'un nouveau langage
Programmer en Java
1. Un premier exemple
1.1. Aperçu général, cycle de vie
1.2. Un premier programme
1.3. Programmes, applets, servlets, etc...
2. Une première classe
2.1. Écriture d'une classe
2.2. Instanciation de cette classe
3. Types de base, classes et objets
3.1. Types de base
3.2. Classes et objets
Classes Object et String
1. Introduction
2. La classe Object
2.1. La méthode toString()
2.2. La méthode clone()
2.3. La méthode equals()
2.4. La méthode hashCode()
2.5. La méthode finalize()
2.6. La méthode getClass()
3. La classe String
3.1. Introduction
3.2. Construction d'un objet de type String
3.3. Concaténation, StringBuffer et StringBuilder
3.4. Concaténations de chaînes de caractères depuis Java 8
3.5. Concaténations de chaînes de caractères depuis Java 11
3.6. Extraction d'une sous-chaîne de caractères
3.7. Comparaison de deux chaînes de caractères
3.8. Méthodes de comparaisons lexicographiques
3.9. Méthode de recherche de caractères
3.10. Méthode de modification de chaîne
3.11. Méthode de duplication
3.12. Support de l'unicode, internationalisation
Structure d'une classe
1. Introduction
2. Classes
2.1. Classes publiques
2.2. Classes internes
2.3. Classe membre
2.4. Classes locales
2.5. Classes anonymes
2.6. Le mot-clé this
3. Éléments statiques
3.1. Champ statique
3.2. Cas des constantes
3.3. Bloc statique
3.4. Classe membre statique
4. Membres d'une classe, visibilité
4.1. Bloc non statique
4.2. Accès à un membre, visibilité
4.3. Les champs
4.4. Signature d'une méthode
4.5. Les méthodes
4.6. Getters et Setters
5. Constructeur, instanciation
5.1. Chargement d'une classe
5.2. Constructeurs d'une classe
5.3. Instanciation d'un objet
5.4. Destruction d'objets
5.5. Le mot-clé final
6. Énumérations
6.1. Déclaration d'une énumération
6.2. Classe énumération
6.3. Méthode toString()
6.4. Méthode valueOf()
6.5. Méthode values()
6.6. Méthode ordinal()
6.7. Méthode compareTo()
6.8. Constructeurs privés
6.9. Classe utilitaire : EnumSet
Noms, opérateurs, tableaux
1. Introduction
2. Identificateurs, noms et expressions
2.1. Identificateurs et noms
2.2. Expressions
3. Opérateurs, ordre d'exécution
3.1. Ordre d'exécution
3.2. Les opérateurs
3.3. Les opérateurs ++ et --
3.4. Les opérateurs % et /
3.5. Les opérateurs <<, >> et >>>
3.6. L'opérateur instanceof
3.7. Les opérateurs &, | et ^
3.8. Les opérateurs && et ||
3.9. L'opérateur ? ... :
3.10. Les opérateurs d'affectation
4. Tableaux
4.1. Création d'un tableau
4.2. Initialisation d'un tableau
4.3. Utilisation d'un tableau comme un Object
4.4. Tableaux de tableaux
4.5. Copie de tableaux
5. Blocs, boucles et contrôles
5.1. Blocs
5.2. Mots-clés réservés
5.3. Tests : if et switch
5.4. Boucles : for, while, do ... while
5.5. Commandes continue et break
5.6. Commandes return et goto
Nombres, précision, calculs
1. Introduction
2. Calculs
2.1. Précision
2.2. Codage des nombres flottants
2.3. Le mot-clé strictfp
2.4. Conversion de types
3. Dépassements de capacité
3.1. Cas des entiers
3.2. Cas des flottants
3.3. Bibliothèques BigInteger et BigDecimal
4. Fonctions mathématiques
4.1. Fonctions usuelles
4.2. Générateurs aléatoires
5. Classes enveloppe
5.1. Associer les types de base à des objets
5.2. Auto-boxing
Héritage, abstraction, interfaces
1. Introduction
2. Abstraction et encapsulation
2.1. Abstraction
2.2. Encapsulation
3. Héritage
3.1. Définition de l'héritage
3.2. Conséquences pour les membres
3.3. Polymorphisme
3.4. Empêcher l'héritage
4. Classes abstraites
5. Interfaces
5.1. Introduction
5.2. Définition
5.3. Java 8 et les interfaces
5.4. Utilisation des interfaces
5.5. Définition de constantes dans les interfaces
5.6. Utilité des interfaces
Packages
1. Introduction
2. Notion de paquet
2.1. Déclaration d’appartenance à un paquet
2.2. Chargement d’une classe
2.3. Choix de nom
3. Archives, chemin de recherche, classpath
3.1. Archives
3.2. Variable CLASSPATH
3.3. Notion de classloader
3.4. Bilan sur les classes chargées
3.5. Visibilité
3.6. Conseils d'écriture, bibliothèque standard
Exceptions
1. Introduction
2. Erreurs et Exceptions
2.1. Classe Throwable, notion de stack trace
2.2. Classe Error
2.3. Classe RuntimException
2.4. Classe Exception
3. Déclenchement d'une exception
3.1. Exceptions déclenchées par la JVM
3.2. Exceptions déclenchées par l'application
4. Capter une exception
4.1. Traiter une exception localement
4.2. Code de captage
5. Créer ses propres exceptions
Entrées / sorties
1. Introduction
2. Notion de fichier
2.1. Introduction
2.2. La classe File
2.3. Construction d'une instance de File
2.4. Méthodes exposées
3. Flux de sortie
3.1. Introduction, notion de flux
3.2. Écriture de caractères, classe Writer
3.3. Bufferisation, construction d'un flux sur un autre
3.4. Utilisation de PrintWriter
3.5. Écriture d'octets, OuputStream
3.6. Écriture de types primitifs : DataOutputStream
3.7. Écriture d'objets : ObjectOutputStream
4. Flux d'entrée
4.1. Introduction
4.2. Lecture de caractères, classe Reader
4.3. Bufferisation, lecture ligne par ligne
4.4. Lecture d'octets : InputStream
4.5. Lecture de types primitifs : DataInputStream
4.6. Lecture d'objets : ObjectInputStream
5. Lecture et écriture de flux croisés
5.1. Introduction
5.2. Lire des caractères sur un flux binaire : classe InputStreamReader
5.3. Écrire des caractères sur un flux binaire : classe OutputStreamWriter
5.4. Remarque sur les jeux de caractères
6. Serialization d'objets
6.1. Enjeu de la sérialization d'objets
6.2. Serialization d'un objet
6.3. Sérialization d'une grappe d'objets
6.4. Première surcharge : méthode writeObject() readObject()
6.5. Deuxième surcharge : utilisation d'un externalizer
6.6. Troisième surcharge : utilisation d'un objet proxy
7. Flux compressés
7.1. Introduction
7.2. Flux de type gzip
7.3. Flux de type zip