|
~Les mot-clés louches de Java

Le langage Java contient peu de mot-clés. On en dénombre un peu moins de cinquantes. Ceci est très peu comparé à d'autres
langages tels que le Visual Basic (plus de 100) ou le C# (environ 75). Pourtant, parmi les mot-clés classiques, les for, if
et consort, se trouvent trois mot-clés plus exotiques, dont l'utilisation n'est que très rarement enseignée aux débutants.
Nos trois compères se nomment strictfp, transient et volatile.


Sans doute le plus méconnu des mot-clés de Java. Ce mot clé, qui est une abréviation de Strict floating point, s'applique
en tant que modificateur d'accès. Ou plus simplement, on l'utilise de la même manière que les mot-clés public ou
synchrnonized. Avec quelques restrictions.
Utilisation
strictfp s'applique en tant que modificateurs de classes, de méthodes ou d'interfaces. L'entité affectée est alors dite
"FP-strict".
public strictfp class FP
{
}
protected strictfp interface FPInterface
{
}
public class FPTest
{
private strictfp foo()
{
}
}
Une des caractéristiques de ce mot-clé est qu'il est délégué à la descendance de l'entité impliquée. Prenons pour exemple le
code source suivant:
public strictfp class FPDelegate
{
void bar()
{
}
}
Dans le code précédent, le fait que la classe FPDelegate soit FP-strict implique que sa méthode bar le soit également.
A l'inverse le fait que la classe soit public n'impose pas que sa méthode le soit !
Mais strictfp ne peut pas s'appliquer aux méthodes d'interfaces, ni
aux constructeurs de classes (à l'inverse de public par exemple):
protected interface FooBar
{
strictfp doFooBar();
}
public class Bar
{
public strictfp Bar()
{
}
}
Effet
Qu'apporte exactement l'utilisation de strictfp ? Comme son nom
l'indique, strictfp agit sur les opérations en virgule flottante. C'est à dire
sur les types primitifs double et float. Voici une opération
en virgule flottante:
public strictfp class FPDemo
{
public static void main(String[] args)
{
double d = 8e+307;
System.out.println(4.0 * d * 0.5);
System.out.println(2 * d);
}
}
Ce programme écrit dans la console les calculs 4 * 8e+307 * 0.5 et
2 * 8e+307, ce qui, normalement, devrait produire le même résultat. Pourtant
la première ligne affiche Infinity et la seconde 1.6e+308. Le type
double possède pour limite supérieur 1.8e+308.
Or Java effectue les calculs en garantissant une priorité de la gauche vers
la droite. Le calcul 4 * d * 0.5 est interprété ainsi: (4 * d) * 0.5. Il s'avère que
le résultat de 4 * d est égal à 3.2e+308. Ce résultat est supérieur à la limite
du type double. Java interprète donc ce résultat comme l'infini.
Ensuite, et à cause du mot-clé strictfp, l'application du calcul * 0.5 ne
change rien, puisque nous multiplions par l'infini. En effet, ce mot-clé impose
à la JVM d'évaluer la suite du calcul sans rien changer.
Notons que le mot-clé oblige l'implémentation de la JVM a évaluer
l'ensemble de l'expression. Ne pas faire usage de ce mot-clé ne garantit
pas que la JVM réalisera ce calcul de la sorte. Une JVM peut en effet avoir
le droit, si la méthode n'est pas FP-strict, d'effectuer le calcul intermédiaire
4 * d dans un type capable de contenir le résultat avant de faire la suite
du calcul. Ainsi le calcul 4 * d * 0.5 pourra, sur une JVM donnée, afficher
le même résultat que 2 * d.
En conclusion, nous avons vu que le mot-clé strictfp permettait de
garantir les mêmes calculs, quelle que soit la machine virtuelle sur laquelle
l'opération est effectuée.
Le mot-clé transient est lié à la sérialisation des classes Java. Mais
avant de nous étendre sur le sujet, rappelons en quelques mots ce qu'est
la sérialisation.
Java, tout comme de nombreux langages, permet de sauvegarder l'état d'un
objet à un instant donné. Cette sauvegarde se fait généralement dans simple
fichier texte. L'intérêt est majeur. Imaginer que vous programmiez un jeu et
que vous possédiez une classe Player. Pour sauvegarder la partie
vous pourrez sérialiser une instance de la classe Player dans un
fichier. Lorsque le joueur décidera de reprendre sa partie, vous serez en
mesure de recréer une instance de Player telle qu'elle était lors
de la sauvegarde.
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.io.FileInputStream;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.io.IOException;
public class Writer
{
Writer()
{
try
{
writeObject();
readObject();
} catch (Exception ioe) {
System.err.println("Erreur: " + ioe.toString());
}
}
private void readObject() throws Exception
{
FileInputStream file = new FileInputStream("writeable.ser");
ObjectInputStream in = new ObjectInputStream(file);
Writeable w;
w = (Writeable) in.readObject();
System.out.println("demo: " + w.demo + " - ghost: " + w.ghost);
file.close();
}
private void writeObject() throws IOException
{
FileOutputStream file = new FileOutputStream("writeable.ser");
ObjectOutputStream out = new ObjectOutputStream(file);
Writeable w = new Writeable();
out.writeObject(w);
System.out.println("demo: " + w.demo + " - ghost: " + w.ghost);
out.flush();
file.close();
}
public static void main(String[] args)
{
new Writer();
}
}
Le code précédent va créer un objet Writeable et le sérialiser dans un
fichier. Ensuite, nous allons créer une nouvelle instance de Writeable
à partir du fichier nouvellement créé. Voici le code de l'objet
Writeable:
class Writeable implements java.io.Serializable
{
public int demo = 4;
public int ghost = 19;
}
Ici l'objet ne propose aucun élément transient. L'exécution du
programme créera le fichier writeable.ser et affichera les deux lignes
suivantes:
demo: 4 - ghost: 19
demo: 4 - ghost: 19
La première ligne correspond aux valeurs par défaut lorsque l'on crée,
instancie l'objet, via new. La second ligne correspond aux valeurs
attribuées lors de la création de l'objet depuis le fichier de sérialisation.
Transformons la classe Writeable pour lui faire utiliser le mot-clé
transient:
class Writeable implements java.io.Serializable
{
public transient int demo = 4;
public int ghost = 19;
}
En exécutant de nouveau le programme, nous voyons apparaître les lignes:
demo: 4 - ghost: 19
demo: 0 - ghost: 19
Dans le second cas, demo vaut 0 ! Pourtant, le code spécifie bien
que la valeur par défaut de demo est 4. Eh oui, mais l'attribution d'une
valeur par défaut se fait lors de l'instanciation de l'objet ! Or, la méthode
consistant à lire un objet depuis un fichier ne crée pas cet instance
explicitement. Donc demo n'est jamais initialisé avec sa valeur par
défaut. De plus, comme cet attribut est transient, il n'est pas écrit
dans le fichier (comparez la taille des deux fichiers). Cela implique que
demo ne reçoit aucune valeur et contient donc 0.
Ce mot-clé trouve des applications dès lors qu'une donnée sensible ne doit
en aucun cas apparaître dans un fichier. Un mot de passe par exemple.
Mais ce mot-clé peut également permettre de "remettre à zéro" certaines
valeurs. Dans le cas d'un jeu, on pourra ainsi ne pas sauvegarder le temps
de jeu depuis le début de la partie.
Toute application un tant soit peu importante fait appel aux threads. Les
threads permettent de simuler l'exécution simultanée de plusieurs tâches.
Le processeur répartit le temps d'exécution entre les divers threads. Ceux-ci
s'exécutent petit bout par petit bout, les uns à la suite des autres. On a alors
l'illusion d'une exécution en parallèle (l'illusion devient réalité dans le cas
de machines possédant plusieurs processeurs). Le mot-clé volatile
intervient dans le cadre des threads en s'appliquant aux variables.
Les machines virtuelles Java possèdent deux emplacements dans lesquelles
elles placent les valeurs en cours d'exécution: le tas et la pile. Les valeurs
placées dans la pile sont accessibles beaucoup plus rapidement car
les machines virtuelles sont optimisées dans cette optique. Or, lesdites
machines virtuelles effectuent souvent des optimisations consistant à
déplacer des valeurs du tas vers la pile. Pourtant de telles optimisations ont
parfois de gros désavantages. Considérons le code source suivant:
public boolean doLoop = true;
while (doLoop);
Ce code, à déconseiller (:-))), entre dans une boucle infinie et n'en sors que
lorsque la variable doLoop vaut false. Or si vous tentez de
modifier la valeur de cette variable depuis un autre thread, cela ne marchera
pas. Par exemple, vous avez une classe contenant cette boucle. Cette même
classe crée une fenêtre avant de lancer la boucle. Vous concevez votre
application de telle sort qu'en appuyant sur un bouton, la boucle se
termine. Malheureusement, les fenêtres possèdent leur propre thread
d'exécution...
Lorsque votre bouton exécutera le code
maclass.doLoop = false, rien ne changera. La JVM aura
en effet déplacé la valeur de doLoop dans une pile, inaccessible
depuis un autre thread. Lorsque votre bouton sera cliqué, la valeur de
doLoop changera... mais dans le tas et non dans la pile ! Afin
d'éviter cet écueil vous devez déclarer votre variable volatile.
public volatile boolean doLoop = true;
while (doLoop);
Et maintenant votre bouton pourra modifier la variable. Le mot clé
volatile interdit à la JVM d'optimiser la variable en la déplacant dans
la pile. La valeur n'étant donc jamais déplacée dans la pile, le code exécuté
par le bouton modifiera la valeur dans le tas, celle qui sera aussi utilisée
par la boucle while.
Note: insistons encore sur le fait que cet exemple est un très mauvais
exemple de programmation. Si vous tenez à tout prix à réaliser une telle
boucle d'attente, ce que nous vous déconseillons fortement, écrivez-là ainsi:
public volatile boolean doLoop = true;
while (doLoop)
{
Thread.yield();
}
La ligne supplémentaire Thread.yield() met en attente tous les threads
en activité et permet aux autres de s'exécuter. Cette ligne évite de
monopoliser toutes les ressources système.
|