Double initialisation Java
J’ai découvert il y a peu de temps une subtilité dans le fonctionnement du langage Java. Il s’agit du déroulement de l’étape de construction d’une nouvelle instance d’une classe.
Saurez-vous deviner le résultat de l’exécution du programme suivant :
public abstract class ClasseA {
private String parent = "A";
public ClasseA() {
System.out.println(parent);
System.out.println(enfant());
}
public abstract String enfant();
private static class ClasseB extends ClasseA {
private String enfant = "B";
public ClasseB() {
System.out.println(enfant());
}
@Override
public String enfant() {
return this.enfant;
}
}
public static void main(String[] args) {
new ClasseB();
}
}
De manière surprenante, le résultat est le suivant :
A null B
En effet, le déroulement de l’étape de construction semble avoir lieu ainsi :
- Exécution du constructeur parent ;
- Initialisation des attributs ;
- Exécution du corps du constructeur.
Ainsi, lors de l’exécution du corps du constructeur de la classe ClasseA, l’appel à la méthode abstraite enfant() renvoie l’attribut enfant de la classe anonyme. Or, à cet instant l’attribut n’a pas encore été initialisé !
Dès lors, si on tente de résoudre le problème par le code suivant :
public abstract class ClasseA {
private String parent = "A";
public ClasseA() {
System.out.println(parent);
System.out.println(enfant());
}
public abstract String enfant();
private static class ClasseB extends ClasseA {
private String enfant = "B";
public ClasseB() {
System.out.println(enfant());
}
@Override
public String enfant() {
if (this.enfant == null)
this.enfant = "C";
return this.enfant;
}
}
public static void main(String[] args) {
new ClasseB();
}
}
On obtient le résultat :
A C B
Ainsi, alors qu’a priori le code dans la condition if (this.enfant == null) n’est pas exécutable, en réalité il en est tout autre !
Si maintenant on n’initialise pas l’attribut enfant :
public abstract class ClasseA {
private String parent = "A";
public ClasseA() {
System.out.println(parent);
System.out.println(enfant());
}
public abstract String enfant();
private static class ClasseB extends ClasseA {
private String enfant;
// Variable permettant de modifier l'attribut enfant
// à chaque nouvelle initialisation
private static byte i = 0;
public ClasseB() {
System.out.println(enfant());
}
@Override
public String enfant() {
if (this.enfant == null) {
this.enfant = Character.toString((char) (((byte) 'B') + i++));
}
return this.enfant;
}
}
public static void main(String[] args) {
new ClasseB();
}
}
Le résultat est conforme à nos attentes :
A B B
Par contre, si on initialise l’attribut enfant à la valeur null :
public abstract class ClasseA {
private String parent = "A";
public ClasseA() {
System.out.println(parent);
System.out.println(enfant());
}
public abstract String enfant();
private static class ClasseB extends ClasseA {
private String enfant = null;
// Variable permettant de modifier l'attribut enfant
// à chaque nouvelle initialisation
private static byte i = 0;
public ClasseB() {
System.out.println(enfant());
}
@Override
public String enfant() {
if (this.enfant == null) {
this.enfant = Character.toString((char) (((byte) 'B') + i++));
}
return this.enfant;
}
}
public static void main(String[] args) {
new ClasseB();
}
}
Alors le résultat est :
A B C
Il y a donc bien réinitialisation de l’attribut enfant à la valeur null.
Donc, même si a priori l’initialisation ou non d’un attribut à la valeur null pouvait sembler être équivalente, dans les faits cette initialisation peut causer des problèmes. Il est notamment possible de créer de cette manière une instance d’un attribut avant la construction du reste de la classe (attention aux références imbriquées), et également de supprimer cette instance avant exécution du corps du constructeur (attention à la double instanciations).