Ambiguïté de surcharge de méthode avec Java 8 primitives ternaires conditionnelles et non boxées

Ce qui suit est le code compile en Java 7, mais pas openjdk-1.8.0.45-31.b13.fc21.

static void f(Object o1, int i) {}
static void f(Object o1, Object o2) {}

static void test(boolean b) {
    String s = "string";
    double d = 1.0;
    // The supremum of types 'String' and 'double' is 'Object'
    Object o = b ? s : d;
    Double boxedDouble = d;
    int i = 1;
    f(o,                   i); // fine
    f(b ? s : boxedDouble, i); // fine
    f(b ? s : d,           i); // ERROR!  Ambiguous
}

Le compilateur revendique le dernier appel de méthode ambigu.

Si nous changeons le type du deuxième paramètre de f de int à Integer, alors le code compile sur les deux plates-formes. Pourquoi le code posté ne compile-t-il pas dans Java 8?

24
demandé sur Mark Rotteveel 2015-05-08 21:39:31

2 réponses

Considérons d'abord une version simplifiée qui n'a pas de conditionnel ternaire et qui ne compile pas sur la machine virtuelle Java HotSpot (build 1.8.0_25-b17):

public class Test {

    void f(Object o1, int i) {}
    void f(Object o1, Object o2) {}

    void test() {
        double d = 1.0;

        int i = 1;
        f(d, i); // ERROR!  Ambiguous
    }
}

L'erreur du compilateur est:

Error:(12, 9) java: reference to f is ambiguous
both method f(java.lang.Object,int) in test.Test and method f(java.lang.Object,java.lang.Object) in test.Test match

Selon JLS 15.12.2. Étape De Compilation 2: Déterminer La Signature De La Méthode

Une méthode est applicable si elle est applicable par une invocation stricte (§15.12.2.2), une invocation lâche (§15.12.2.3) ou une invocation d'arité variable (§15.12.2.4).

L'Invocation a à voir avec le contexte d'invocation qui est expliqué ici JLS 5.3. Contexte D'Invocation

Quand aucune boxe ou unboxing n'est impliqué pour une invocation de méthode alors l'invocation stricte s'applique. Lorsque la boxe ou le déballage est impliqué pour une invocation de méthode, l'invocation lâche s'applique.

L'identification des méthodes applicables est divisée en 3 phases.

La première phase (§15.12.2.2) effectue une résolution de surcharge sans permettre conversion de boxe ou de déballage, ou l'utilisation de l'invocation de la méthode d'arité variable. si aucune méthode applicable n'est trouvée pendant cette phase, le traitement se poursuit jusqu'à la deuxième phase.

La deuxième phase (§15.12.2.3) effectue une résolution de surcharge tout en permettant la boxe et le déballage, mais empêche toujours l'utilisation de l'invocation de la méthode d'arité variable. si aucune méthode applicable n'est trouvée pendant cette phase, le traitement se poursuit jusqu'à la troisième phase.

La troisième phase (§15.12.2.4) permet de combiner la surcharge avec des méthodes d'arité variable, de boxe et de déballage.

Pour notre cas, il n'y a pas de méthodes applicables par invocation stricte. Les deux méthodes sont applicables par invocation lâche puisque la double valeur doit être encadrée.

Selon JLS 15.12.2.5 choisir la méthode la plus spécifique :

Si plusieurs méthodes membres sont à la fois accessibles et applicables à méthode invocation, il est nécessaire d'en choisir un pour fournir la descripteur pour la répartition de la méthode d'exécution. Le langage de programmation Java langue utilise la règle la plus spécifique de la méthode choisie.

Puis:

Une méthode applicable m1 est plus spécifique qu'une autre applicable méthode m2, pour une invocation avec des expressions d'argument e1,..., ek, si l'un des éléments suivants est vrai:

  1. M2 est générique, et m1 est supposé être plus spécifique que m2 pour expressions d'argument e1,..., ek par §18.5.4.

  2. M2 n'est pas générique, et m1 et m2 sont applicables par strictes ou plus souples l'invocation, et où m1 a des types de paramètres formels S1, ..., Sn et m2 a des types de paramètres formels T1, ..., Tn, le type Si est plus spécifique que Ti pour l'argument ei pour tout I (1 ≤ i ≤ n, n = k).

  3. M2 n'est pas générique, et m1 et m2 sont applicables par arité variable l'invocation, et où la première variable K arity paramètre types de m1 sont S1, ..., Sk et le premier k variable arity paramètre types de m2 sont T1, ..., Tk, le type Si est plus spécifique que Ti pour l'argument ei pour tous i (1 ≤ i ≤ k). De plus, si m2 a des paramètres k + 1, alors le type de paramètre d'arité de K + 1'ème variable de m1 est un sous-type K + 1'ème variable arity type de paramètre de m2.

Les conditions ci-dessus sont les seules circonstances dans lesquelles une méthode peut être plus spécifique qu'une autre.

Un type S est plus spécifique qu'un type T pour n'importe quelle expression si S <:>

, Il peut sembler que la condition 2 matches dans ce cas, mais en fait, il n'est pas possible parce que int n'est pas un sous-type de l'Objet: il n'est pas vrai que int <:>Objet. Cependant, si nous remplaçons int par Integer dans la signature de la méthode f, cette condition correspondrait. Notez que le 1er paramètre dans methods correspond à cette condition depuis l'objet <:>L'objet est vrai.

Selon $4.10 pas de la relation sous-type / supertype est définie entre les types primitifs et les types de classe / Interface. Donc int n'est pas un sous-type de Object, par exemple. Donc int n'est pas plus spécifique que Object.

Puisque parmi les 2 méthodes il n'y a pas de méthodes plus spécifiques donc il ne peut y avoir aucunestrictement plus spécifique et peut être aucuneméthode la plus spécifique (le JLS donne des définitions pour ces termes dans le même paragraphe JLS 15.12.2.5 Choisir la méthode la plus spécifique ). Donc, les deux méthodes sont au maximum spécifiques .

Dans ce cas, le JLS donne 2 options:

Si toutes les méthodes les plus spécifiques ont des signatures équivalentes de substitution (§8.4.2)...

Ce n'est pas notre cas, donc

Sinon, l'invocation de la méthode est ambiguë et une erreur de compilation se produit.

L'erreur de compilation pour notre cas semble valide selon le JLS.

Que se passe-t-il si nous changeons le type de paramètre de méthode de int en entier?

Dans ce cas, les deux méthodes sont toujours applicables par invocation libre. Cependant, la méthode avec le paramètre Integer est plus spécifique que la méthode avec 2 Paramètres D'objet depuis Integer <:>

Que se passe t il si nous changeons double en Double dans cette ligne: double d = 1.0;?

Dans ce cas, il y a exactement 1 méthode applicable par invocation stricte: aucune boxe ou unboxing n'est requise pour l'invocation de cette méthode: f (Object o1, int i). Pour l'autre méthode, vous devez faire de la boxe de valeur int afin qu'elle soit applicable par invocation lâche. Le compilateur peut choisir la méthode applicable par invocation stricte ainsi aucune erreur du compilateur n'est levée.

Comme Marco13 l'a souligné dans son commentaire, il y a un cas similaire discuté dans ce post pourquoi cette méthode surcharge-t-elle ambiguë?

Comme expliqué dans la réponse, il y a eu quelques changements majeurs liés aux mécanismes d'invocation de méthode entre Java 7 et Java 8. Ceci explique pourquoi le code compile en Java 7 mais pas en Java 8.


Maintenant vient la partie amusante!

Ajoutons un opérateur conditionnel ternaire:

public class Test {

    void f(Object o1, int i) {
        System.out.println("1");
    }
    void f(Object o1, Object o2) {
        System.out.println("2");
    }

    void test(boolean b) {
        String s = "string";
        double d = 1.0;
        int i = 1;

        f(b ? s : d, i); // ERROR!  Ambiguous
    }

    public static void main(String[] args) {
        new Test().test(true);
    }
}

Le compilateur se plaint de l'invocation ambiguë de la méthode. Le JLS 15.12.2 ne dicte aucun règles spéciales liées aux opérateurs conditionnels ternaires lors de l'exécution d'invocations de méthode.

Cependant, il y a JLS 15.25 opérateur conditionnel ? : et JLS 15.25.3. Référence Des Expressions Conditionnelles . Le premier catégorise les expressions conditionnelles en 3 sous-catégories: booléen, numérique et expression conditionnelle de référence. Les deuxième et troisième opérandes de notre expression conditionnelle ont respectivement des types String et double. Selon le JLS notre expression conditionnelle est une expression conditionnelle de référence.

Puis selon JLS 15.25.3. Reference Conditional Expressions Notre expression conditionnelle est une expression conditionnelle de poly reference puisqu'elle apparaît dans un contexte d'invocation. Le type de notre expression conditionnelle poly est donc Object (le type cible dans le contexte d'invocation). De là nous pourrions continuer les étapes comme si le premier paramètre était Object auquel cas le compilateur devrait choisir la méthode avec int comme deuxième paramètre (et ne pas lancer l'erreur du compilateur).

La partie délicate est cette note de JLS:

Ses deuxième et troisième expressions d'opérande apparaissent de la même manière dans un contexte du même type avec le type cible T.

De ceci nous pouvons supposer (aussi le "poly" dans le nom l'implique) que dans le contexte de l'invocation de méthode les 2 opérandes devraient être considérés indépendamment. Ce que cela signifie est que lorsque le compilateur doit décider si un boxe l'opération est requise pour un tel argument, il devrait examiner chacun des opérandes et voir si une boxe peut être nécessaire. Pour notre cas spécifique, la chaîne ne nécessite pas de boxe et le double nécessitera de la boxe. Ainsi, le compilateur décide que pour les deux méthodes surchargées, il devrait s'agir d'une invocation de méthode lâche. Les autres étapes sont les mêmes que dans le cas où au lieu d'une expression conditionnelle ternaire, nous utilisons une double valeur.

D'après l'explication ci-dessus, il semble que le JLS lui-même est vague et ambigu dans la partie liée aux expressions conditionnelles lorsqu'elles sont appliquées à des méthodes surchargées, nous avons donc dû faire des hypothèses.

Ce qui est intéressant, c'est que mon IDE (IntelliJ IDEA) ne détecte pas le dernier cas (avec l'expression conditionnelle ternaire) comme une erreur de compilateur. Tous les autres cas qu'il détecte selon le compilateur java de JDK. Cela signifie que le compilateur java JDK ou l'analyseur IDE interne a un bug.

15
répondu medvedev1088 2017-05-23 12:17:05

En bref:

Le compilateur ne sait pas quelle méthode choisir car l'ordre entre les types primitifs et de référence n'est pas défini dans JLS en ce qui concerne le choix de la méthode la plus spécifique.

Lorsque vous utilisez Integer au lieu de int, le compilateur choisit la méthode avec Integer car Integer est un sous-type D'objet.

Lorsque vous utilisez Double au lieu de double, le compilateur choisit la méthode qui n'implique pas de boxe ou de déballage.

Avant Java 8, les règles étaient différent donc ce code pourrait compiler.

0
répondu fkn 2015-05-09 20:10:24