Quand le résultat d'un code Java me surprend, je regarde son bytecode avec javap -c.
Prenons un exemple. Dans notre équipe, nous mettons les variables à final
par défaut pour éviter des problèmes de concurrence [1]. J'ai découvert[2] que final
pouvait changer le comportement de l'opérateur ternaire ? :
, comme le montre ce code :
public static void main(String[] args) {
final int s1 = 6;
int s2 = 6;
System.out.println(false ? s1 : 'X');
System.out.println(false ? s2 : 'X');
}
qui affiche :
X 88
Comment diable 'X' a-t-il été convertit en entier alors que le résultat est connu à la compilation ? Quand je ne comprends pas, je lis le bytecode (javap -c) :
public static void main(java.lang.String[]);
Code:
Stack=2, Locals=3, Args_size=1
0: bipush 6
1: istore_2
2: getstatic #2; //Field java/lang/System.out:Ljava/io/PrintStream;
5: bipush 88
7: invokevirtual #3; //Method java/io/PrintStream.println:(C)V
10: getstatic #2; //Field java/lang/System.out:Ljava/io/PrintStream;
13: bipush 88
15: invokevirtual #4; //Method java/io/PrintStream.println:(I)V
18: return
Décomposons les instructions du premier println (
2:
à 7:
) :
2: getstatic récupère le champs static System.out 5: bipush 88 empile 88, valeur ascii de X 7: invokevirtual invoque la méthode println
De 10: à 15:
le second println a l'air identique, mais à y regarder de plus près le type du paramètre diffère : (I) au lieu de (C) :
- 7: invokevirtual #3; //Method java/io/PrintStream.println:(C)V
- 15: invokevirtual #4; //Method java/io/PrintStream.println:(I)V
Le virtual de invokevirtual indique un appel polymorphe... et la méthode println va s'appliquer à un type int (I) alors que la première invocation prends pour type char (C)
L'énigme #8 de Java Puzzlers[3] nous fournit l'explication par les spécifications de l'opération ternaire ?s1:'X'
- le résultat est de type int quand parmi les deux opérandes (s1 et 'X') on a
- un de type char
- l'autre une constante int représentable en char
- sinon la promotion numérique s'applique
Le point déterminant est que la constante soit représentable sous forme de char. Si l'entier constant est trop grand ou trop petit, la promotion entière vers 88 s'applique :
final int tropGrand = Character.MAX_VALUE + 1;
final int tropPetit = Character.MIN_VALUE - 1;
System.out.println(false ? tropGrand : 'X'); // Affiche 88
System.out.println(false ? tropPetit : 'X'); // Affiche 88
Le conseil donné dans Java Puzzlers : avoir deux opérandes du même type, ce qui évite tout problème.
Références :
- [1] Java concurrency in practice de Brian Goetz et al
- [2] Java bien!
- [3] Java Puzzlers de Joshua Bloch and Neal Gafter
Note sur le bytecode bipush :
Contrairement au puzzle #8, j'affecte 6 aux entiers plutôt que zéro car le bytecode dépend de l'entier affecté :
- de -1 à 5 : iconst (
int i = 0
devienticonst_0
) - de 6 à 127 : bipush (
int i = 6
devientbipush 6
) - à partir de 128 : sipush (
int i = 128
devientsipush 128
)
Pour faciliter la compréhension des non-initiés, je préfère garder le même bytecode pour les affectations de 6 et 88.