2015-06-03 9 views
20

Il seguente programma viene compilato in Java 7 e in Eclipse Marte RC2 per Java 8:La variabile di inferenza ha limiti incompatibili. Regressione del compilatore 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); 
     } 
    } 
} 

Utilizzando il compilatore javac 1.8.0_45, viene segnalato il seguente errore di compilazione:

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 ? 

Una soluzione è localmente assegnare una variabile:

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); 
     } 
    } 
} 

so che l'inferenza dei tipi è cambiata molto in Java 8 (e.g. due to JEP 101 "generalized target-type inference"). Quindi, questo è un bug o una nuova "funzione" linguistica?

EDIT: Ho anche segnalato questo a Oracle come JI-9.021.550, ma solo nel caso in cui questa è una "caratteristica" in Java 8, ho segnalato il problema anche a Eclipse:

+0

hi Lukas, non chiamiamo ancora un bug di eclissi; per favore deseleziona la mia risposta :( – ZhongYu

+0

@ bayou.io: hai ragione Fatto! –

risposta

6

grazie per la bug report, e grazie, Holger, per l'esempio nella vostra risposta. Questi e molti altri mi hanno fatto finalmente mettere in discussione una piccola modifica apportata al compilatore di Eclipse 11 anni fa. Il punto era: Eclipse aveva esteso illegalmente l'algoritmo di acquisizione per applicare ricorsivamente ai limiti dei caratteri jolly.

C'è stato un esempio in cui questo cambiamento illegale ha allineato perfettamente il comportamento di Eclipse con javac. Generazioni di sviluppatori di Eclipse si sono fidati di questa vecchia decisione più di quello che potremmo vedere chiaramente in JLS. Oggi credo che la deviazione precedente debba aver avuto una ragione diversa.

Oggi ho preso il coraggio di allineare ecj con JLS a questo proposito e voilà 5 bug che sembravano estremamente difficili da decifrare, sono stati essenzialmente risolti proprio così (oltre a un piccolo aggiustamento qua e là come compensazione).

Ergo: Sì, Eclipse aveva un bug, ma il bug è stato risolto a partire dal 4.7 Milestone 2 :)

Ecco cosa ecj riferirà d'ora in poi:

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

E 'il carattere jolly all'interno un limite di acquisizione che non trova una regola per rilevare la compatibilità. Più precisamente, un po 'di tempo durante l'inferenza (incorporazione per la precisione) si verifica il seguente vincolo (T # 0 che rappresenta una variabile di deduzione):

⟨T#0 = ?⟩ 

Ingenuamente, potremmo semplicemente risolvere la variabile di tipo per il carattere jolly, ma - - presumibilmente perché i caratteri jolly non sono considerati tipi - le regole di riduzione definiscono quanto sopra come riducente a FALSE, lasciando così fallire l'inferenza.

7

Disclaimer - io non ne so abbastanza su questo argomento, e il seguente è un ragionamento informale del mio per cercare di giustificare il comportamento di javac.


possiamo ridurre il problema ad

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

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

Dedurre T, abbiamo vincoli

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

In sostanza, questo è irrisolvibile. Ad esempio, non esiste T se X=List<?>.

Non sono sicuro di come Java7 deduca questo caso. Ma javac8 (e IntelliJ) si comportano "ragionevolmente", direi.


Ora, come mai questa soluzione funziona?

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

Funziona grazie alla cattura jolly, che introduce ulteriori informazioni Gruppo, "restringendo" il tipo di instance

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

Purtroppo, questo non avviene quando instance è X, quindi c'è meno tipo informazioni con cui lavorare

In teoria, la lingua potrebbe essere "migliorato" per fare la cattura jolly per X troppo:

instance is X, X is List<?> => exist W, where instance is List<W> 
+0

Interessante ... In effetti, Eclipse rifiuta anche il tuo esempio, quindi forse questa riduzione non è esattamente equivalente? Il metodo 'void a()' è generico, per esempio: perché questo è irrisolvibile? Se 'X = Lista ' allora 'T = CAP # 1 'sarebbe un' T' valido, no? Cosa mi manca? Perché l'assegnazione di una variabile locale funziona come soluzione alternativa?Nel tuo caso, potrei anche assegnare 'List instance = type.newInstance()' e funzionerebbe di nuovo. –

+1

durante l'inferenza non è stata acquisita alcuna wildcard. 'X <:List' non implica che esiste 'W dove X <:List'; in realtà tale 'W' non deve esistere. – ZhongYu

+1

il tipo di oggetto 'type' deve essere sottoposto a cattura con caratteri jolly (diventando' Classe ') prima dell'inferenza. Ricordo che Eclipse potrebbe essere un po 'diverso su questo. – ZhongYu

5

Grazie alla bayou.io’s answer possiamo restringere il problema del fatto che

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

produce un errore mentre

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

possono essere compilati senza problemi. L'assegnazione di instance2=instance è una conversione di ampliamento che dovrebbe verificarsi anche per gli argomenti di richiamo del metodo. Quindi la differenza con il modello di this answer è la relazione di sottotipo addizionale.


notare che, mentre io non sono sicuro se questo caso specifico è in linea con il linguaggio Java Specification, alcuni test hanno rivelato che Eclipse accettare il codice è probabilmente dovuto al fatto che è più sciatto quanto riguarda i tipi generici in generale, come il seguente, sicuramente errata, codice potrebbe essere compilato senza alcun errore né avvertimento:

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)); 
}