web-dev-qa-db-fra.com

La variable d'inférence a des limites incompatibles. Java 8 Régression du compilateur?

Le programme suivant se compile en Java 7 et dans Eclipse Mars RC2 pour Java 8:

import Java.util.List;

public class Test {

    static final void a(Class<? extends List<?>> type) {
        b(newList(type));
    }

    static final <T> List<T> b(List<T> list) {
        return list;
    }

    static final <L extends List<?>> L newList(Class<L> type) {
        try {
            return type.newInstance();
        }
        catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

À l'aide du compilateur javac 1.8.0_45, l'erreur de compilation suivante est signalée:

Test.Java:6: error: method b in class Test cannot be applied to given types;
        b(newList(type));
        ^
  required: List<T>
  found: CAP#1
  reason: inference variable L has incompatible bounds
    equality constraints: CAP#2
    upper bounds: List<CAP#3>,List<?>
  where T,L are type-variables:
    T extends Object declared in method <T>b(List<T>)
    L extends List<?> declared in method <L>newList(Class<L>)
  where CAP#1,CAP#2,CAP#3 are fresh type-variables:
    CAP#1 extends List<?> from capture of ? extends List<?>
    CAP#2 extends List<?> from capture of ? extends List<?>
    CAP#3 extends Object from capture of ?

Une solution de contournement consiste à affecter localement une variable:

import Java.util.List;

public class Test {

    static final void a(Class<? extends List<?>> type) {

        // Workaround here
        List<?> variable = newList(type);
        b(variable);
    }

    static final <T> List<T> b(List<T> list) {
        return list;
    }

    static final <L extends List<?>> L newList(Class<L> type) {
        try {
            return type.newInstance();
        }
        catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

Je sais que l'inférence de type a beaucoup changé dans Java 8 ( par exemple en raison de JEP 101 "inférence généralisée de type cible" ). Donc, est-ce un bogue ou une nouvelle "fonctionnalité" de langue?

[~ # ~] modifier [~ # ~] : J'ai également signalé cela à Oracle sous le nom JI-9021550, mais juste au cas où il s'agit d'un " feature "in Java 8, j'ai également signalé le problème à Eclipse:

21
Lukas Eder

Merci pour le rapport de bogue , et merci, Holger, pour l'exemple dans votre réponse. Ceux-ci et plusieurs autres m'ont finalement fait remettre en question un petit changement apporté au compilateur Eclipse il y a 11 ans. Le point était le suivant: Eclipse avait illégalement étendu l'algorithme de capture pour l'appliquer récursivement aux limites génériques.

Il y avait un exemple où ce changement illégal alignait parfaitement le comportement d'Eclipse avec javac. Des générations de développeurs Eclipse ont davantage fait confiance à cette ancienne décision qu'à ce que nous pouvions clairement voir dans JLS. Aujourd'hui, je pense que la déviation antérieure a dû avoir une raison différente.

Aujourd'hui, j'ai pris le courage d'aligner ecj avec JLS à cet égard et voila 5 bugs qui semblaient être extrêmement difficiles à résoudre, ont essentiellement été résolus comme ça (plus un petit Tweak ici et là en compensation).

Ergo: Oui, Eclipse avait un bug, mais ce bug a été corrigé à partir du 4.7 jalon 2 :)

Voici ce que ECJ rapportera désormais:

The method b(List<T>) in the type Test is not applicable for the arguments (capture#1-of ? extends List<?>)

C'est le caractère générique dans une limite de capture qui ne trouve pas de règle pour détecter la compatibilité. Plus précisément, un certain temps au cours de l'inférence (incorporation pour être précis) nous rencontrons la contrainte suivante (T # 0 représentant une variable d'inférence):

⟨T#0 = ?⟩

Naïvement, nous pourrions simplement résoudre la variable de type en caractère générique, mais - vraisemblablement parce que les caractères génériques ne sont pas considérés comme des types - les règles de réduction définissent ce qui précède comme se réduisant à FAUX, laissant ainsi l'inférence échouer.

6
Stephan Herrmann

Avis de non-responsabilité - Je ne connais pas suffisamment le sujet, et voici un raisonnement informel pour essayer de justifier le comportement de Javac.


Nous pouvons réduire le problème à

<X extends List<?>> void a(Class<X> type) throws Exception
{
    X instance = type.newInstance();
    b(instance);  // error
}

<T> List<T> b(List<T> list) { ... }

Pour déduire T, nous avons des contraintes

      X <: List<?>
      X <: List<T>

Essentiellement, cela est insoluble. Par exemple, aucun T n'existe si X=List<?>.

Je ne sais pas comment Java7 infère ce cas. Mais javac8 (et IntelliJ) se comporte "raisonnablement", je dirais.


Maintenant, comment se fait-il que cette solution de contournement fonctionne?

    List<?> instance = type.newInstance();
    b(instance);  // ok!

Cela fonctionne grâce à la capture de caractères génériques, qui introduit plus d'informations sur le type, "restreignant" le type de instance

    instance is List<?>  =>  exist W, where instance is List<W>  =>  T=W

Malheureusement, cela n'est pas fait lorsque instance est X, donc il y a moins d'informations de type avec lesquelles travailler.

En théorie, le langage pourrait être "amélioré" pour effectuer également une capture générique pour X:

    instance is X, X is List<?>  =>  exist W, where instance is List<W>
7
ZhongYu

Grâce à réponse de bayou.io nous pouvons limiter le problème au fait que

<X extends List<?>> void a(X instance) {
    b(instance);  // error
}
static final <T> List<T> b(List<T> list) {
    return list;
}

produit une erreur en

<X extends List<?>> void a(X instance) {
    List<?> instance2=instance;
    b(instance2);
}
static final <T> List<T> b(List<T> list) {
    return list;
}

peut être compilé sans problème. L'affectation de instance2=instance est une conversion élargie qui devrait également se produire pour les arguments d'invocation de méthode. Ainsi, la différence avec le modèle de cette réponse est la relation de sous-type supplémentaire.


Notez que même si je ne suis pas sûr que ce cas spécifique soit conforme à la spécification de langage Java, certains tests ont révélé qu'Eclipse acceptant le code est probablement dû au fait qu'il est plus bâclé en ce qui concerne Les types génériques en général, comme le code suivant, certainement incorrect, peuvent être compilés sans erreur ni avertissement:

public static void main(String... arg) {
    List<Integer> l1=Arrays.asList(0, 1, 2);
    List<String>  l2=Arrays.asList("0", "1", "2");
    a(Arrays.asList(l1, l2));
}
static final void a(List<? extends List<?>> type) {
    test(type);
}
static final <Y,L extends List<Y>> void test(List<L> type) {
    L l1=type.get(0), l2=type.get(1);
    l2.set(0, l1.get(0));
}
5
Holger