Booléens, opérateurs conditionnels et autoboxing

132

Pourquoi ce jette NullPointerException

public static void main(String[] args) throws Exception {
    Boolean b = true ? returnsNull() : false; // NPE on this line.
    System.out.println(b);
}

public static Boolean returnsNull() {
    return null;
}

alors que ce n'est pas

public static void main(String[] args) throws Exception {
    Boolean b = true ? null : false;
    System.out.println(b); // null
}

?

La solution est d'ailleurs de remplacer falsepar Boolean.FALSEpour éviter d' nullêtre déballé boolean- ce qui n'est pas possible. Mais ce n'est pas la question. La question est pourquoi ? Existe-t-il des références dans JLS qui confirment ce comportement, en particulier du 2ème cas?

BalusC
la source
28
wow, la boxe automatique est une source infinie de ... euh ... surprises pour le programmeur java, n'est-ce pas? :-)
leonbloy
J'ai eu un problème similaire et ce qui m'a surpris, c'est qu'il a échoué sur la VM OpenJDK mais a fonctionné sur HotSpot VM ... Écrivez une fois, exécutez n'importe où!
kodu

Réponses:

92

La différence est que le type explicite de la returnsNull()méthode affecte le typage statique des expressions au moment de la compilation:

E1: `true ? returnsNull() : false` - boolean (auto-unboxing 2nd operand to boolean)

E2: `true ? null : false` - Boolean (autoboxing of 3rd operand to Boolean)

Voir Spécification du langage Java, section 15.25 Opérateur conditionnel? :

  • Pour E1, les types des 2ème et 3ème opérandes sont Booleanet booleanrespectivement, donc cette clause s'applique:

    Si l'un des deuxième et troisième opérandes est de type booléen et le type de l'autre est de type booléen, alors le type de l'expression conditionnelle est booléen.

    Puisque le type de l'expression est boolean, le 2ème opérande doit être forcé boolean. Le compilateur insère du code de déballage automatique dans le 2ème opérande (valeur de retour de returnsNull()) pour le faire taper boolean. Cela provoque bien sûr le NPE du nullretourné au moment de l'exécution.

  • Pour E2, les types des 2ème et 3ème opérandes sont <special null type>(pas Booleancomme dans E1!) Et booleanrespectivement, donc aucune clause de typage spécifique ne s'applique ( allez les lire! ), Donc la clause finale "sinon" s'applique:

    Sinon, les deuxième et troisième opérandes sont respectivement de types S1 et S2. Soit T1 le type qui résulte de l'application de la conversion de boxe à S1, et soit T2 le type qui résulte de l'application de la conversion de boxe à S2. Le type de l'expression conditionnelle est le résultat de l'application de la conversion de capture (§5.1.10) en lub (T1, T2) (§15.12.2.7).

    • S1 == <special null type>(voir §4.1 )
    • S2 == boolean
    • T1 == box (S1) == <special null type>(voir le dernier élément de la liste des conversions de boxe au §5.1.7 )
    • T2 == box (S2) == `Booléen
    • lub (T1, T2) == Boolean

    Ainsi, le type de l'expression conditionnelle est Booleanet le 3ème opérande doit être contraint Boolean. Le compilateur insère du code de boxing automatique pour le 3ème opérande ( false). Le 2ème opérande n'a pas besoin du déballage automatique comme dans E1, donc pas de NPE de déballage automatique lorsqu'il nullest renvoyé.


Cette question nécessite une analyse de type similaire:

Opérateur conditionnel Java?: Type de résultat

Bert F
la source
4
Ça a du sens ... je pense. Le §15.12.2.7 est une douleur.
BalusC
C'est facile ... mais seulement avec le recul. :-)
Bert F
@BertF Que signifie la fonction luben question lub(T1,T2)?
Geek
1
@Geek - lub () - moindre borne supérieure - fondamentalement la superclasse la plus proche qu'ils ont en commun; puisque null (type "le type spécial nul") peut être implicitement converti (élargi) en n'importe quel type, vous pouvez considérer le type nul spécial comme une "superclasse" de n'importe quel type (classe) pour les besoins de lub ().
Bert F
25

La ligne:

    Boolean b = true ? returnsNull() : false;

est transformé en interne en:

    Boolean b = true ? returnsNull().booleanValue() : false; 

pour effectuer le déballage; donc: null.booleanValue()donnera un NPE

C'est l'un des principaux écueils lors de l'utilisation de l'auto-box. Ce comportement est en effet documenté dans 5.1.8 JLS

Edit: Je pense que le déballage est dû au fait que le troisième opérateur est de type booléen, comme (cast implicite ajouté):

   Boolean b = (Boolean) true ? true : false; 
jjungnickel
la source
2
Pourquoi essaie-t-il de déballer comme ça, alors que la valeur ultime est un objet booléen?
Erick Robertson
16

À partir de la spécification du langage Java, section 15.25 :

  • Si l'un des deuxième et troisième opérandes est de type booléen et le type de l'autre est de type booléen, alors le type de l'expression conditionnelle est booléen.

Ainsi, le premier exemple essaie d'appeler Boolean.booleanValue()afin de convertir Booleanà booleanselon la première règle.

Dans le second cas, le premier opérande est de type nul, lorsque le second n'est pas de type référence, la conversion automatique est donc appliquée:

  • Sinon, les deuxième et troisième opérandes sont respectivement de types S1 et S2. Soit T1 le type qui résulte de l'application de la conversion de boxe à S1, et soit T2 le type qui résulte de l'application de la conversion de boxe à S2. Le type de l'expression conditionnelle est le résultat de l'application de la conversion de capture (§5.1.10) en lub (T1, T2) (§15.12.2.7).
axtavt
la source
Cela répond au premier cas, mais pas au second cas.
BalusC
Il existe probablement une exception lorsque l'une des valeurs est null.
Erick Robertson
@Erick: JLS le confirme-t-il?
BalusC
1
@Erick: Je ne pense pas que ce soit applicable car ce booleann'est pas un type de référence.
axtavt
1
Et puis-je ajouter ... c'est pourquoi vous devriez faire des deux côtés d'un ternaire le même type, avec des appels explicites si nécessaire. Même si vous avez mémorisé les spécifications et que vous savez ce qui va se passer, le prochain programmeur à venir lire votre code pourrait ne pas le faire. À mon humble avis, il serait préférable que le compilateur produise simplement un message d'erreur dans ces situations plutôt que de faire des choses difficiles à prévoir pour les mortels ordinaires. Eh bien, il y a peut-être des cas où le comportement est vraiment utile, mais je n'en ai pas encore atteint un.
Jay
0

Nous pouvons voir ce problème à partir du code d'octet. À la ligne 3 du code d'octet principal,, 3: invokevirtual #3 // Method java/lang/Boolean.booleanValue:()Zle booléen de boxe de valeur null, invokevirtualla méthode java.lang.Boolean.booleanValue, il lancera NPE bien sûr.

    public static void main(java.lang.String[]) throws java.lang.Exception;
      descriptor: ([Ljava/lang/String;)V
      flags: ACC_PUBLIC, ACC_STATIC
      Code:
        stack=2, locals=2, args_size=1
           0: invokestatic  #2                  // Method returnsNull:()Ljava/lang/Boolean;
           3: invokevirtual #3                  // Method java/lang/Boolean.booleanValue:()Z
           6: invokestatic  #4                  // Method java/lang/Boolean.valueOf:(Z)Ljava/lang/Boolean;
           9: astore_1
          10: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
          13: aload_1
          14: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
          17: return
        LineNumberTable:
          line 3: 0
          line 4: 10
          line 5: 17
      Exceptions:
        throws java.lang.Exception

    public static java.lang.Boolean returnsNull();
      descriptor: ()Ljava/lang/Boolean;
      flags: ACC_PUBLIC, ACC_STATIC
      Code:
        stack=1, locals=0, args_size=0
           0: aconst_null
           1: areturn
        LineNumberTable:
          line 8: 0
Yanhui Zhou
la source