Vous aimez ce que vous lisez sur ce blog ?
Envie d'aller plus loin avec véritable formation d'expertise en Java ?
Venez suivre ma formation Masterclasse Expertise Java !

"Même un développeur experimenté a besoin de continuer à apprendre. Et dans cette formation... j'ai appris beaucoup !" - A.G., Java Champion

Sessions intra-entreprises sur demande : contact[at]mokatech.net.
Inscrivez-vous vite !

Java : Les "forward references"

On appelle forward reference le fait d'utiliser une variable avant même qu'elle ne soit déclarée. Le langage Java autorise cette pratique, mais lui impose des restrictions strictes, notamment pour éviter tout problème de référence circulaire.

Nous allons expliquer ces règles, puis voir comment elles sont appliquées et peuvent être contournées - et quels effets de bord cela peut entraîner.

Un premier exemple

Tout d'abord, un exemple de forward reference (FwdRef par la suite) :

  1. public class PremierExemple
  2. {
  3. int a = z;
  4. int z = 1;
  5. }

Ici, a référence z avant sa déclaration, le compilateur indique donc l'erreur suivante : "Cannot reference a field before it is defined". Mais ce qu'il doit surtout empêcher, c'est une référence circulaire :

  1. public class PremierExemple
  2. {
  3. int a = z;
  4. int z = a;
  5. }


Définition d'une FwdRef illégale

La Java Language Specification (JLS) spécifie trois règles principales permettant d'identifier une FwdRef illégale au sein d'une classe C donnée :

  1. Elle a lieu pendant l'initialisation d'une variable de C ou pendant l'exécution d'un bloc d'initialisation d'instance de C. Cette règle s'applique également aux les variables/blocs d'initialisation statiques.
  2. La variable forward-référencée est accédée en lecture
  3. La variable forward-référencée est accédée directement (c'est-à-dire uniquement par son nom, sans aucune indirection)

Dans le premier exemple donné plus haut, les trois conditions sont vérifiées :

  1. on est en train d'initialiser la variable a,
  2. on essaie de lire z,
  3. on accède à z directement par son nom, et pas par déréférencement


Examen des règles

Règle #1

Règle : La FwdRef doit avoir lieu durant l'initialisation d'une variable ou dans un bloc d'initialisation.
Pourquoi : Ce n'est qu'à l'issue du processus d'initialisation que les variables sont considérées comme réellement utilisables.

Pour les variables statiques, ce processus d'initialisation se décompose en deux phases :

  1. Les variables statiques prennent leur valeur par défaut (0 pour les entiers, 0.0 pour les flottants, false pour les booléens, null pour les objets...)
  2. Dans l'ordre de déclaration (de haut en bas du code, donc), les variables statiques sont initialisées si une valeur leur est affectée, et les blocs d'initialisation statique sont exécutés.

Deux phases similaires existent pour les variables d'instance, déclenchées lors de l'instanciation de la classe.

Le risque d'utiliser une variable non encore initialisée est donc circonscrit à ces deux phases, ce qui explique pourquoi la notion même de FwdRef n'a de sens que dans ce contexte.

Prenons quelques exemples :

  1. /* Ne compile pas */
  2. public class Exemple1
  3. {
  4. static int a = b;
  5. static int b = 1;
  6.  
  7. int x = z;
  8. int z = 1
  9. }
  10.  
  11. /* Ne compile pas */
  12. public class Exemple2
  13. {
  14. static int a;
  15. static { a = b; }
  16. static int b = 1;
  17.  
  18. int x;
  19. { x = z; }
  20. int z = 1;
  21. }
  22.  
  23. /* Compile : le constructeur ne fait pas partie du processus d'initialisation */
  24. public class Exemple3
  25. {
  26. int x;
  27. public Exemple3()
  28. {
  29. x = z;
  30. }
  31. int z = 1;
  32. }

Règle #2

Règle : on essaie de lire la variable forward-référencée.
Pourquoi : Lors de la seconde phase du processus d'initialisation, les variables sont déclarées mais pas encore totalement initialisées. Il est donc possible de leur affecter une valeur (puisqu'elles sont déclarées), mais pas de les lire.

Exemples :

  1. /* Ne compile pas car on essaie de lire "b" */
  2. public class Exemple4
  3. {
  4. static int a = b;
  5. static int b = 1;
  6. }
  7.  
  8. /* Compile puisqu'on ne lit pas "b" : on lui affecte une valeur */
  9. public class Exemple5
  10. {
  11. static int a = (b=3);
  12. static int b = 1;
  13. }

Effet de bord :
Si on affecte une valeur à une variable forward-référencée, cette valeur sera écrasée lorsque la JVM parviendra à la ligne de code effectuant l'initialisation de cette variable.

  1. /* Compile et affiche 2 puis 4 : la valeur de "b" a bien été affectée temporairement puis écrasée */
  2. public class Exemple6
  3. {
  4. static int a = (b=2);
  5. static { System.out.println(a); }
  6. static int b = 4;
  7. static { System.out.println(b); }
  8. }

Règle #3

Règle : on accède à la variable forward-référencée directement par son nom.
Pourquoi : Les variables accédées via des références (statiques ou non) ou renvoyées par des méthodes ne sont pas vérifiées. Tant que la variable n'est pas totalement initialisée, sa valeur lue par ce moyen est sa valeur par défaut.

Démonstration avec un accès via la classe:

  1. public class Exemple7
  2. {
  3. static int a = Exemple7.b + 1;
  4. static int b = a + 2;
  5. static { System.out.println("a,b = "+a+","+b); }
  6. }

On pourrait croire qu'il existe une dépendance circulaire, car a référence b, qui référence à son tour a. Pourtant, il n'en est rien.

Une variable forward-référencée ne peut pas être utilisée directement par d'autres variables partageant sa phase d'initialisation, car elle n'a pas encore acquis sa valeur finale.
En revanche, pour du code extérieur y accédant par déréférencement, cette phase d'initialisation se comporte comme une transaction : pendant toute son exécution, ils ne voient que l'ancienne valeur, qu'ils peuvent donc utiliser sans danger - mais avec un effet de bord similaire à celui vu plus haut.

Examinons l'exemple 7 :

  • Après la première phase, a et b sont déclarés et possèdent la valeur par défaut 0 puisque ce sont des int.
  • Au début de la seconde phase, on accède à b via une référence. A cet instant, b n'est pas encore totalement initialisée (ce sera fait à la seconde ligne) et la valeur lue est donc sa valeur par défaut, c'est-à-dire 0.
  • L'expression "Exemple7.b + 1" est calculée, et vaut 0+1 = 1.Cette valeur est assignée à la variable a.
  • A la ligne suivante, l'expression "a + 2" est calculée, et vaut 1+2 = 3.
  • Cette valeur est assignée à la variable b, qui est désormais totalement initialisée.
  • La console affiche "a,b = 1,3" comme prévu.

Un autre exemple, utilisant un accès via une méthode :

  1. /* Compile, affiche "a,b = 0,1" */
  2. public class Exemple8
  3. {
  4. static int getB() { return b; }
  5. static int a = getB();
  6. static int b = 1;
  7. static { System.out.println("a,b = "+a+","+b); }
  8. }

Dans cet exemple, a vaut 0 car il a lu (via la méthode getB()) la valeur de b avant son initialisation à la valeur 1.


Conclusion

Les programmeurs estiment souvent qu'il n'est pas nécessaire d'initialiser explicitement les variables qu'ils déclarent, si la valeur à affecter est la même que la valeur par défaut du type.

  1. public class Exemple9
  2. {
  3. int a; /* Equivalent à : int a = 0; ? */
  4. }

Pourtant, il suffit d'utiliser cette variable comme FwdRef pour s'apercevoir que les deux formules ne sont pas équivalentes :

  1. public class Exemple10
  2. {
  3. static { Exemple10.a = 1; }
  4. static int a;
  5. static { System.out.println(a); } /* Affiche 1 */
  6.  
  7. static { Exemple10.b = 1; }
  8. static int b=0;
  9. static { System.out.println(b); } /* Affiche 0 */
  10. }

Ajouter un commentaire

Le code HTML est affiché comme du texte et les adresses web sont automatiquement transformées.