web-dev-qa-db-fra.com

Différence de comportement de l'opérateur ternaire sur les JDK8 et JDK10

Considérons le code suivant

public class JDK10Test {
    public static void main(String[] args) {
        Double d = false ? 1.0 : new HashMap<String, Double>().get("1");
        System.out.println(d);
    }
}

Lorsqu'il est exécuté sur JDK8, ce code imprime null alors que sur JDK10, ce code donne NullPointerException

Exception in thread "main" Java.lang.NullPointerException
    at JDK10Test.main(JDK10Test.Java:5)

Le bytecode produit par les compilateurs est presque identique, à l'exception de deux instructions supplémentaires générées par le compilateur JDK10 qui sont liées à l'autoboxing et semblent être responsables du NPE.

15: invokevirtual #7                  // Method Java/lang/Double.doubleValue:()D
18: invokestatic  #8                  // Method Java/lang/Double.valueOf:(D)Ljava/lang/Double;

Ce comportement est-il un bogue dans JDK10 ou une modification intentionnelle visant à rendre le comportement plus strict?

JDK8:  Java version "1.8.0_172"
JDK10: Java version "10.0.1" 2018-04-17
57
SerCe

Je crois que c'était un bug qui semble avoir été corrigé. Lancer le NullPointerException semble être le comportement correct, selon le JLS.

Je pense que ce qui se passe ici est que pour une raison quelconque, dans la version 8, le compilateur a pris en compte les limites de la variable de type mentionnée par le type de retour de la méthode plutôt que les arguments de type réels. En d'autres termes, il pense que ...get("1") renvoie Object. Cela pourrait être dû au fait qu'il envisage l'effacement de la méthode ou à une autre raison.

Le comportement devrait dépendre du type de retour de la méthode get, comme indiqué dans les extraits ci-dessous de §15.26 :

  • Si les deuxième et troisième expressions d'opérande sont des expressions numériques, l'expression conditionnelle est une expression conditionnelle numérique.

    Aux fins de la classification d'un conditionnel, les expressions suivantes sont des expressions numériques:

    • […]

    • Expression d'appel de méthode (§15.12) pour laquelle la méthode la plus spécifique choisie (§15.12.2.5) possède un type de retour convertible en un type numérique.

      Notez que, pour une méthode générique, il s'agit du type avant instancier les arguments de type de la méthode.

    • […]

  • Sinon, l'expression conditionnelle est une expression conditionnelle de référence.

[…]

Le type d'une expression conditionnelle numérique est déterminé comme suit:

  • […]

  • Si l'un des deuxième et troisième opérandes est de type primitif T et que le type de l'autre est le résultat de l'application de la conversion de boxe (§5.1.7) à T, le type de l'expression conditionnelle est T.

En d'autres termes, si les deux expressions sont convertibles en un type numérique, et que l'une est primitive et que l'autre est encadrée, le type de résultat du conditionnel ternaire est le type primitif.

(Le tableau 15.25-C nous indique également que le type d'une expression ternaire boolean ? double : Double Serait bien de double, ce qui signifie encore une fois que le déballage et le lancement sont corrects.)

Si le type de retour de la méthode get n'était pas convertible en un type numérique, le conditionnel ternaire serait alors considéré comme une "expression conditionnelle de référence" et le déballage ne se produirait pas.

De plus, je pense que la note "pour une méthode générique, c'est le type avant d'instancier les arguments de type de la méthode" ne devrait pas s'appliquer à notre cas. Map.get Ne déclare pas les variables de type, donc ce n'est pas une méthode générique selon la définition de JLS . Cependant, cette note était ajoutée dans Java 9 (étant le seul changement, voir JLS8 ), il est donc possible qu'elle soit quelque chose à voir avec le comportement que nous observons aujourd'hui.

Pour un HashMap<String, Double>, Le type de retour de get devrait être Double.

Voici un MCVE supportant ma théorie selon laquelle le compilateur considère les limites de variable de type plutôt que les arguments de type réels:

class Example<N extends Number, D extends Double> {
    N nullAsNumber() { return null; }
    D nullAsDouble() { return null; }

    public static void main(String[] args) {
        Example<Double, Double> e = new Example<>();

        try {
            Double a = false ? 0.0 : e.nullAsNumber();
            System.out.printf("a == %f%n", a);
            Double b = false ? 0.0 : e.nullAsDouble();
            System.out.printf("b == %f%n", b);

        } catch (NullPointerException x) {
            System.out.println(x);
        }
    }
}

La sortie de ce programme sur Java 8 est:

a == null
Java.lang.NullPointerException

En d'autres termes, bien que e.nullAsNumber() et e.nullAsDouble() aient le même type de retour réel, seul e.nullAsDouble() est considéré comme une "expression numérique". La seule différence entre les méthodes est la variable de type liée.

Il y a probablement plus d'enquêtes à faire, mais je voulais publier mes résultats. J'ai essayé pas mal de choses et trouvé que le bogue (c'est-à-dire pas unboxing/NPE) ne semble se produire que lorsque l'expression est une méthode avec une variable de type dans le type de retour.


Fait intéressant, j'ai trouvé que le programme suivant lance également dans Java 8:

import Java.util.*;

class Example {
    static void accept(Double d) {}

    public static void main(String[] args) {
        accept(false ? 1.0 : new HashMap<String, Double>().get("1"));
    }
}

Cela montre que le comportement du compilateur est en réalité différent, selon que l'expression ternaire est affectée à une variable locale ou à un paramètre de méthode.

(Au départ, je voulais utiliser des surcharges pour prouver le type réel que le compilateur donne à l'expression ternaire, mais cela ne semble pas possible, vu la différence susmentionnée. Il est possible qu'il existe une autre façon à laquelle je n'ai pas pensé, bien que.)

47
Radiodef

JLS 10 ne semble pas spécifier de modifications à l'opérateur conditionnel, mais j'ai une théorie.

Selon JLS 8 et JLS 10, si la deuxième expression (1.0) Est de type double et que la troisième (new HashMap<String, Double>().get("1")) est de type Double , alors le résultat de l’expression conditionnelle est de type double. La machine virtuelle Java dans Java 8 semble être assez intelligente pour le savoir, car vous renvoyez un Double, il n’ya aucune raison de déballer d’abord le résultat de HashMap#get vers un double et le replacer ensuite dans un Double (car vous avez spécifié Double).

Pour le prouver, changez Double en double dans votre exemple et un NullPointerException est lancé (dans JDK 8); c'est parce que le déballage est en cours, et null.doubleValue() jette évidemment un NullPointerException.

double d = false ? 1.0 : new HashMap<String, Double>().get("1");
System.out.println(d); // Throws a NullPointerException

Il semble que cela ait été changé en 10, mais je ne peux pas vous dire pourquoi.

12
Jacob G.