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 devient iconst_0)
  • de 6 à 127 : bipush (int i = 6 devient bipush 6)
  • à partir de 128 : sipush (int i = 128 devient sipush 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.