2016-06-29 46 views
41

Ho incappato in un pezzo di codice che ha mi chiedo il motivo per cui viene compilato correttamente:Perché questo codice generico viene compilato in java 8?

public class Main { 
    public static void main(String[] args) { 
     String s = newList(); // why does this line compile? 
     System.out.println(s); 
    } 

    private static <T extends List<Integer>> T newList() { 
     return (T) new ArrayList<Integer>(); 
    } 
} 

Ciò che è interessante è che se modifico la firma del metodo di newList con <T extends ArrayList<Integer>> non funziona più.

aggiornamento dopo i commenti & risposte: Se sposto il tipo generico dal metodo alla classe il codice non fa più a compilare:

public class SomeClass<T extends List<Integer>> { 
    public void main(String[] args) { 
     String s = newList(); // this doesn't compile anymore 
     System.out.println(s); 
    } 

    private T newList() { 
     return (T) new ArrayList<Integer>(); 
    } 
} 
+3

correlati http://stackoverflow.com/questions/36402646/generic-return-type-upper-bound-interface-vs-class-surprisingly-valid-code – Tunaki

+1

Il motivo dell'aggiornamento che fa la differenza è che il primo la versione può funzionare se esiste un tipo che soddisfa i vincoli (T estende la lista e T estende String) [cioè è una quantificazione esistenziale], mentre la seconda versione può funzionare solo se tutti i possibili tipi T che corrispondono alla dichiarazione nella classe estendono anche String [vale a dire è una quantificazione universale]. –

risposta

32

Se si dichiara un parametro di tipo a un metodo, si consente al chiamante di scegliere un tipo effettivo per esso, fino a quando quel tipo effettivo soddisfare i vincoli. Quel tipo non deve essere un concreto tipo concreto, potrebbe essere un tipo astratto, una variabile di tipo o un tipo di intersezione, in altre parole più colloquiali, un tipo ipotetico. Quindi, as said by Mureinik, potrebbe esserci un tipo che si estende String e che implementa List. Non possiamo specificare manualmente un tipo di intersezione per l'invocazione, ma possiamo usare una variabile di tipo per dimostrare la logica:

public class Main { 
    public static <X extends String&List<Integer>> void main(String[] args) { 
     String s = Main.<X>newList(); 
     System.out.println(s); 
    } 

    private static <T extends List<Integer>> T newList() { 
     return (T) new ArrayList<Integer>(); 
    } 
} 

Naturalmente, newList() non può soddisfare l'aspettativa di restituire un tale tipo, ma questo è il problema della definizione (o implementazione) di questo metodo. Dovresti ricevere un avviso "non selezionato" quando lanci il numero da ArrayList a T. L'unica implementazione corretta potrebbe restituire null qui, che rende il metodo abbastanza inutile.

Il punto, per ripetere l'istruzione iniziale, è che il chiamante di un metodo generico sceglie un tipo generico per i parametri del tipo . Al contrario, quando si dichiara una generica classe come con

public class SomeClass<T extends List<Integer>> { 
    public void main(String[] args) { 
     String s = newList(); // this doesn't compile anymore 
     System.out.println(s); 
    } 

    private T newList() { 
     return (T) new ArrayList<Integer>(); 
    } 
} 

il parametro di tipo fa parte del contratto della classe, così anche colui che crea un'istanza sceglierà i tipi effettivi per tale istanza. Il metodo di istanza main fa parte di quella classe e deve rispettare tale contratto.Non puoi scegliere lo T che desideri; il tipo effettivo per T è stato impostato e in Java, di solito non è nemmeno possibile scoprire cosa sia T.

Il punto chiave della programmazione generica è scrivere codice che funzioni indipendentemente da quali tipi effettivi sono stati scelti per i parametri di tipo.

Ma si noti che è possibile creare , un'altra istanza indipendente con qualsiasi tipo desiderato e invocare il metodo, ad esempio

public class SomeClass<T extends List<Integer>> { 
    public <X extends String&List<Integer>> void main(String[] args) { 
     String s = new SomeClass<X>().newList(); 
     System.out.println(s); 
    } 

    private T newList() { 
     return (T) new ArrayList<Integer>(); 
    } 
} 

Qui, il creatore della nuova istanza seleziona i tipi effettivi per quell'istanza. Come detto, quel tipo effettivo non ha bisogno di essere un tipo concreto.

17

Sto indovinando questo è perché è un List interfaccia. Se ignoriamo il fatto che String è final per un secondo, è possibile, in teoria, avere una classe che è extends String (che significa che è possibile assegnarlo a s) ma implements List<Integer> (che potrebbe essere restituito da newList()). Una volta modificato il tipo di ritorno da un'interfaccia (T extends List) a una classe concreta (T extends ArrayList), il compilatore può dedurre che non sono assegnabili tra loro e produce un errore.

Questo, ovviamente, si interrompe dal String è, infatti, final, e potremmo aspettarci che il compilatore ne tenga conto. IMHO, è un bug, anche se devo ammettere che non sono un esperto di compilatori e potrebbe esserci una buona ragione per ignorare il modificatore final a questo punto.

+1

Hmm, questo è davvero un buon punto. Non pensavo alla possibilità di avere una classe che potesse in teoria estendere sia String che implementare List. Come hai detto, questo non si applica in quanto String è definitivo, ma è un'idea interessante. –

+7

Il fatto che una classe sia 'final' in fase di compilazione non viene mai preso in considerazione in situazioni come non c'è garanzia che la classe sarà ancora' finale' in fase di esecuzione. – Holger

+0

@errantlinguist Non sono sicuro di seguire quello che stai dicendo. Il problema è che il codice presentato viene compilato quando assegno un valore di tipo T estende List a un valore di tipo String. –

6

Non so perché questa compilazione. D'altra parte, posso spiegare come è possibile sfruttare appieno i controlli in fase di compilazione.

Quindi, newList() è un metodo generico, ha un parametro di tipo. Se si specifica questo parametro, il compilatore verificherà che per voi:

fallisce la compilazione:

String s = Main.<String>newList(); // this doesn't compile anymore 
System.out.println(s); 

passa la fase di compilazione:

List<Integer> l = Main.<ArrayList<Integer>>newList(); // this compiles and works well 
System.out.println(l); 

Specificando thetype parametro

T I parametri di tipo forniscono solo il controllo in fase di compilazione. Questo è in base alla progettazione, java utilizza type erasure per i tipi generici. Per far funzionare il compilatore per te, devi specificare quei tipi nel codice.

parametro Type in grado di creazione

Il caso più comune è quello di specificare i modelli di un'istanza di oggetto. Cioè per gli elenchi:

List<String> list = new ArrayList<>(); 

Qui possiamo vedere che List<String> specifica il tipo per le voci di elenco. D'altra parte, nuovo ArrayList<>() no. Utilizza invece lo diamond operator. Cioè il compilatore java infers il tipo basato sulla dichiarazione.

parametro di tipo implicito alla chiamata di metodo

Quando si richiama un metodo statico, allora è necessario specificare il tipo in un altro modo. A volte è possibile specificare come parametro:

public static <T extends Number> T max(T n1, T n2) { 
    if (n1.doubleValue() < n2.doubleValue()) { 
     return n2; 
    } 
    return n1; 
} 

La si può usare in questo modo:

int max = max(3, 4); // implicit param type: Integer 

O come questa:

double max2 = max(3.0, 4.0); // implicit param type: Double 

espliciti parametri di tipo a chiamata di metodo:

Per esempio, questo è il modo in cui può creare un tipo-safe elenco vuoto:

List<Integer> noIntegers = Collections.<Integer>emptyList(); 

Il tipo di parametro <Integer> viene passato al metodo emptyList(). L'unico vincolo è che devi specificare anche la classe. Cioè Non si può fare questo: tipo

import static java.util.Collections.emptyList; 
... 
List<Integer> noIntegers = <Integer>emptyList(); // this won't compile 

Runtime gettone

Se nessuno di questi trucchi può aiutare, allora è possibile specificare un runtime type token. Cioè fornisci una classe come parametro. Un esempio comune è l'EnumMap:

private static enum Letters {A, B, C}; // dummy enum 
... 
public static void main(String[] args) { 
    Map<Letters, Integer> map = new EnumMap<>(Letters.class); 
} 
+0

Potresti semplificare la spiegazione? –

+0

La risposta semplice è che è possibile specificare i parametri del tipo all'interno delle parentesi "<>".Altrimenti ho reso la spiegazione più lunga in modo da poter vedere tutte le alternative. –