2013-07-03 13 views
20

Il seguente programma di test è derivato da un programma più complicato che fa qualcosa di utile. Compila con successo con il compilatore Eclipse.È possibile dedurre argomenti di tipo Java dai limiti dei parametri di tipo?

import java.util.ArrayList; 
import java.util.List; 

public class InferenceTest 
{ 
    public static void main(String[] args) 
    { 
     final List<Class<? extends Foo<?, ?>>> classes = 
      new ArrayList<Class<? extends Foo<?, ?>>>(); 
     classes.add(Bar.class); 
     System.out.println(makeOne(classes)); 
    } 

    private static Foo<?, ?> makeOne(Iterable<Class<? extends Foo<?, ?>>> classes) 
    { 
     for (final Class<? extends Foo<?, ?>> cls : classes) 
     { 
      final Foo<?, ?> foo = make(cls); // javac error here 
      if (foo != null) 
       return foo; 
     } 
     return null; 
    } 

    // helper used to capture wildcards as type variables 
    private static <A, B, C extends Foo<A, B>> Foo<A, B> make(Class<C> cls) 
    { 
     // assume that a real program actually references A and B 
     try 
     { 
      return cls.getConstructor().newInstance(); 
     } 
     catch (final Exception e) 
     { 
      return null; 
     } 
    } 

    public static interface Foo<A, B> {} 

    public static class Bar implements Foo<Integer, Long> {} 
} 

Tuttavia, con l'Oracle JDK 1.7 javac, non riesce con questo:

InferenceTest.java:18: error: invalid inferred types for A,B; inferred type does not 
conform to declared bound(s) 
      final Foo<?, ?> foo = make(cls); 
            ^
    inferred: CAP#1 
    bound(s): Foo<CAP#2,CAP#3> 
    where A,B,C are type-variables: 
    A extends Object declared in method <A,B,C>make(Class<C>) 
    B extends Object declared in method <A,B,C>make(Class<C>) 
    C extends Foo<A,B> declared in method <A,B,C>make(Class<C>) 
    where CAP#1,CAP#2,CAP#3 are fresh type-variables: 
    CAP#1 extends Foo<?,?> from capture of ? extends Foo<?,?> 
    CAP#2 extends Object from capture of ? 
    CAP#3 extends Object from capture of ? 
1 error 

quale compilatore è giusto?

Un aspetto sospetto dell'output precedente è CAP#1 extends Foo<?,?>. Mi aspetto che il tipo di limiti variabili sia CAP#1 extends Foo<CAP#2,CAP#3>. In questo caso, il limite dedotto di CAP#1 si conformerebbe ai limiti dichiarati. Tuttavia, questo potrebbe essere una falsa pista, a causa C dovrebbe effettivamente essere dedotto di essere CAP#1, ma il messaggio di errore è per quanto riguarda A e B.


Nota che se sostituisco linea 26 con la seguente, entrambi i compilatori accettano il programma:

private static <C extends Foo<?, ?>> Foo<?, ?> make(Class<C> cls) 

Tuttavia, ora non può fare riferimento ai tipi catturate dei parametri Foo.

Aggiornamento: Analogamente accettato da entrambe compilatori (ma anche inutile) è presente:

private static <A, B, C extends Foo<? extends A, ? extends B>> 
    Foo<? extends A, ? extends B> make(Class<C> cls) 

Essa provoca essenzialmente A e B essere banalmente dedurre come Object, e quindi ovviamente non utile in qualsiasi contesto. Tuttavia, conferisce credibilità alla mia teoria al di sotto del fatto che javac eseguirà solo l'inferenza sui limiti dei caratteri jolly, non sui limiti di acquisizione. Se nessuno ha idee migliori, questa potrebbe essere la (sfortunata) risposta. (End Modifica)


mi rendo conto tutta questa domanda è probabile TL; DR, ma io continuerò nel caso in cui qualcun altro sta colpendo questo numero ...

Sulla base di JLS 7, §15.12.2.7 Inferring Type Arguments Based on Actual Arguments , ho fatto la seguente analisi:

Dato un vincolo della forma , A = F, o A >> F:

Inizialmente, abbiamo un vincolo del modulo , che indica che il tipo A è convertibile in tipo F mediante la conversione del metodo di conversione (§5.3). Qui, A è Class<CAP#1 extends Foo<CAP#2, CAP#3>> e F è Class<C extends Foo<A, B>>. Notare che gli altri moduli di vincolo (A = F e A >> F) si presentano solo quando l'algoritmo di inferenza ricorre.

Avanti, C deve dedurre essere CAP#1 dalle seguenti regole:

(2.) Altrimenti, se il vincolo ha la forma :

  • Se F ha la forma G<..., Yk-1, U, Yk+1, ...>, dove U è un'espressione tipo che coinvolge Tj, quindi se A ha un supertipo della forma G<..., Xk-1, V, Xk+1, ...> dove V è un'espressione di tipo, , questo algoritmo viene applicato in modo ricorsivo al vincolo V = U.

Qui, G è Class, U e Tj sono C, e V è CAP#1. applicazione ricorsiva per CAP#1 = C dovrebbe comportare il vincolo C = CAP#1:

(3.) Altrimenti, se il vincolo ha la forma A = F:

  • Se F = Tj, quindi il vincolo Tj = A è implicito.

Fino a questo punto, l'analisi sembra essere d'accordo con l'uscita javac. Forse il punto di divergenza è se continuare a tentare di dedurre A e B. Ad esempio, in questa regola

  • Se F ha la forma G<..., Yk-1, ? extends U, Yk+1, ...>, dove U comporta Tj, quindi se A ha un supertipo che è uno tra:
    • G<..., Xk-1, V, Xk+1, ...>, dove V è un tipo di espressione .
    • G<..., Xk-1, ? extends V, Xk+1, ...>.

Allora questo algoritmo è applicata in modo ricorsivo al vincolo V << U.

Se CAP#1 è considerato un carattere jolly (che è una cattura di), allora questa regola vale, e inferenza continua ricorsivamente con U come Foo<A, B> e V come Foo<CAP#2, CAP#3>. Come sopra, ciò darebbe A = CAP#2 e B = CAP#3.

Tuttavia, se CAP#1 è semplicemente una variabile di tipo, quindi nessuna delle regole sembra considerare i suoi limiti. Forse questa concessione alla fine della sezione nella specifica si riferisce a tali casi: algoritmo di inferenza

Il tipo deve essere visto come un euristico, progettato per funzionare bene in pratica. Se non riesce ad inferire il risultato desiderato, al suo posto possono essere usati parametri di tipo esplicito.

Ovviamente, i caratteri jolly non possono essere utilizzati come parametri di tipo esplicito. :-(

+0

Quale versione di 'javac' stai usando? 'javac -version' – Jeffrey

+0

@Jeffrey: più specificatamente di 1.7? 'javac 1.7.0_25' –

+0

@BevynQ: Sì, quell'esempio" soluzione "era nella mia domanda. Tuttavia, non è utile, poiché il punto di 'make' è quello di acquisire i caratteri jolly, ad es. per rappresentare più istanze dello stesso tipo in ulteriori chiamate di metodo o creando altre istanze di oggetti con tali parametri di tipo. Il problema non ha nulla a che fare con l'assegnazione del risultato a 'Foo '; quella parte funziona in entrambi i compilatori. –

risposta

10

Il problema è che ci si trova nella seguente vincolo inferenza:

class<#1>, #1 <: Foo<?, ?>

che vi dà una soluzione per C, vale a dire C = # 1.

allora avete bisogno di controllare se C è conforme alle limiti dichiarati - il limite di C è Foo, così si finisce con questo controllo:

#1 <: Foo<A,B>

che può essere riscritta come

Bound(#1) <: Foo<A, B>

quindi:

Foo<?, ?> <: Foo<A, B>

Ora, ecco il compilatore fa una conversione cattura del LHS (ecco dove # 2 e # 3 sono generati):

Foo<#2, #3> <: Foo<A, B>

Il che significa

A = #2

B = #3

Quindi, abbiamo che la nostra soluzione è {A = # 2, B = # 3, C = # 1}.

È una soluzione valida?Per rispondere a questa domanda dobbiamo controllare se i tipi inferiti sono compatibili con i limiti variabili inferenza, dopo tipo di sostituzione, quindi:

[A:=#2]A <: Object
#2 <: Object - ok

[B:=#3]B <: Object
#3 <: Object - ok

[C:=#1]C <: [A:=#2, B:=#3]Foo<A, B>
#1 <: Foo<#2, #3>
Foo<#4, #5> <: Foo<#2, #3> - not ok

Quindi l'errore.

La specifica è sottospecificata quando si tratta dell'interazione tra inferenza e tipi catturati, quindi è abbastanza normale (ma non buono!) Avere comportamenti diversi quando si passa da un compilatore all'altro. Tuttavia, alcuni di questi problemi vengono elaborati, sia dal punto di vista del compilatore che dal punto di vista di JLS, quindi problemi come questo dovrebbero essere risolti a medio termine.

+0

Grazie per la rapida risposta! Perché "Foo " viene reintrodotto con una nuova cattura? Se il compilatore esegue una conversione di acquisizione separata nella fase di verifica dalla fase di inferenza, sembra che non si possano mai avere vincoli di inferenza soddisfacenti (non banali) con i caratteri jolly. IOW, # 2 e # 4 corrispondono allo stesso jolly, tuttavia '# 4 <: # 2' non sarà mai vero. Il JLS non sembra richiedere questo (in effetti, nessuna menzione dell'acquisizione in quella sezione) e rompe il codice sensato senza soluzione, quindi è un bug javac, giusto? –

+0

Si tratta di un bug di specifiche: la specifica impone la nuova cattura in primo luogo; la mia opinione è che, a seconda di come viene eseguito il controllo post-inferenza, un determinato compilatore potrebbe essere influenzato da questo comportamento. Javac è certamente. Ma quello che dici è puntato al 100%: la generazione di variabili acquisite durante la sottotipizzazione come parte del processo di inferenza porta a vincoli insoddisfacenti - motivo per cui le specifiche vengono rielaborate in quest'area. –

+0

Grazie, è bello saperlo. Sai se le specifiche funzionano con Java 8, 9 o oltre? Per ora, ho appena usato un cast raw come soluzione, dal momento che ho fiducia di Eclipse che la logica è corretta. –

1

Due cose che ho notato:

  1. CAP#1 non è un jolly, è una variabile di tipo a causa di capture conversion

  2. Al primo passo, il JLS afferma che. U è l'espressione tipo mentre Tj è il tipo di parametro . il JLS non definisce esplicitamente ciò che un tipo di espressione è, ma il mio istinto è che include i limiti del parametro tipo. Se questo è il caso, U sarebbe C extends Foo<A,B> e V sarebbe CAP#1 extends Foo<CAP#2, CAP#3>. In seguito l'algoritmo di inferenza tipo:

V = U ->C = CAP#1 E Foo<CAP#2, CAP#3> = Foo<A, B>

È possibile continuare ad applicare l'algoritmo di inferenza di tipo a quanto sopra, e si finirà con A= CAP#2 e B=CAP#3.

Credo hai trovato un bug con il compilatore di Oracle

+0

D'accordo, 'CAP # n' è una variabile di tipo che viene introdotta a causa della conversione di cattura di un carattere jolly. Quello che stavo ottenendo lì era se la conversione di cattura avviene interamente prima dell'inferenza di tipo. Ci è voluto un po 'per trovarlo, ma la risposta sembra essere sì: "Se il nome dell'espressione appare in un contesto in cui è soggetto alla [...] conversione del richiamo del metodo [...], allora il tipo dell'espressione nome è il tipo dichiarato del campo, variabile locale o parametro dopo la conversione della cattura. [§6.5.6.1] (http://docs.oracle.com/javase/specs/jls/se7/html/jls-6.html # JLS-6.5.6.1) –

Problemi correlati