Come funzionano i farmaci generici?

Mentre capisco alcuni dei casi limite dei generici, mi manca qualcosa con il seguente esempio.

Ho la seguente class

1 public class Test { 2 public static void main(String[] args) { 3 Test t = new Test(); 4 List<Test> l =Collections.singletonList(t); 5 } 6 } 

La riga 4 mi dà l’errore

 Type mismatch: cannot convert from List<Test> to List<Test>`. 

Ovviamente, il compilatore pensa che il diverso ? non sono davvero uguali Mentre il mio istinto mi dice, questo è corretto.

Qualcuno può fornire un esempio in cui vorrei ottenere un errore di runtime se la riga 4 fosse legale?

MODIFICARE:

Per evitare confusione, ho sostituito il =null nella riga 3 con un incarico concreto

Come ha notato Kenny nel suo commento, puoi aggirare questo con:

 List> l = Collections.>singletonList(t); 

Questo ci dice immediatamente che l’operazione non è pericolosa , è solo una vittima di un’inferenza limitata. Se non fosse sicuro, quanto sopra non verrebbe compilato.

Dal momento che l’utilizzo di parametri di tipo esplicito in un metodo generico come sopra è sempre necessario solo per fare da suggerimento , possiamo supporre che la richiesta qui sia una limitazione tecnica del motore di inferenza. In effetti, il compilatore Java 8 è attualmente in distribuzione con molti miglioramenti all’inferenza del tipo . Non sono sicuro se il tuo caso specifico sarà risolto.

Quindi, cosa sta realmente accadendo?

Bene, l’errore di compilazione che stiamo ottenendo mostra che il parametro di tipo T di Collections.singletonList viene dedotto per essere capture> capture> . In altre parole, il carattere jolly ha alcuni metadati associati che lo collegano a un contesto specifico.

  • Il modo migliore per pensare a una cattura di un carattere jolly ( capture ) è come un parametro di tipo senza nome con gli stessi limiti (es. , ma senza essere in grado di fare riferimento a T ).
  • Il modo migliore per “scatenare” la potenza dell’acquisizione è collegarlo a un parametro di tipo named di un metodo generico. Lo dimostrerò in un esempio qui sotto. Vedi il tutorial Java “Metodi di cattura e di supporto di Wildcard” (grazie per il riferimento @WChargin) per ulteriori letture.

Diciamo che vogliamo avere un metodo che sposta una lista, spostandola verso la parte posteriore. Quindi supponiamo che il nostro elenco abbia un tipo sconosciuto (jolly).

 public static void main(String... args) { List list = new ArrayList<>(Arrays.asList("a", "b", "c")); List cycledTwice = cycle(cycle(list)); } public static  List cycle(List list) { list.add(list.remove(0)); return list; } 

Funziona bene, perché T è stato risolto per capture capture , no ? extends String ? extends String . Se invece usassimo questa implementazione non generica del ciclo:

 public static List cycle(List list) { list.add(list.remove(0)); return list; } 

Non riuscirebbe a compilare, perché non abbiamo reso accessibile la cattura assegnandola ad un parametro di tipo.

Quindi questo inizia a spiegare perché il consumatore di singletonList trarrebbe beneficio dal tipo di inferenza che risolve T per Test Test , e quindi restituisce una List>> List>> invece di un List> List> .

Ma perché non è assegnabile all’altro?

Perché non possiamo semplicemente assegnare una List>> List>> a un List> List> ?

Bene se pensiamo al fatto che capture capture è l’equivalente di un parametro di tipo anonimo con un limite superiore di Number , quindi possiamo trasformare questa domanda in “Perché la compilazione seguente non è?” (non è così!):

 public static  List> assign(List> t) { return t; } 

Questo ha una buona ragione per non compilare. Se lo facesse, allora sarebbe ansible:

 //all this would be valid List> doubleTests = null; List> numberTests = assign(doubleTests); Test integerTest = null; numberTests.add(integerTest); //type error, now doubleTests contains a Test 

Allora perché funziona esplicitamente?

Torniamo all’inizio. Se quanto sopra non è sicuro, come mai è permesso:

 List> l = Collections.>singletonList(t); 

Perché funzioni, implica che è permesso quanto segue:

 Test> capturedT; Test t = capturedT; 

Bene, questa non è una syntax valida, dato che non possiamo fare riferimento esplicitamente alla cattura, quindi valutiamola usando la stessa tecnica di cui sopra! Leghiamo l’acquisizione a una variante diversa di “assegna”:

 public static  Test assign(Test t) { return t; } 

Questo compila con successo. E non è difficile capire perché dovrebbe essere sicuro. È il caso di utilizzo di qualcosa di simile

 List l = new List(); 

Non c’è un potenziale errore di runtime, è solo al di fuori della capacità del compilatore di determinarlo staticamente. Ogni volta che si causa un’inferenza di tipo, viene generata automaticamente una nuova cattura di e due acquisizioni non sono considerate equivalenti.

Quindi se rimuovi l’inferenza dall’invocazione di singletonList specificando per esso:

 List> l = Collections.>singletonList(t); 

Funziona bene. Il codice generato non è diverso da se la tua chiamata fosse stata legale, è solo una limitazione del compilatore che non può capirlo da solo.

La regola secondo cui un’inferenza crea un’acquisizione e le acquisizioni non sono compatibili è ciò che blocca questo esempio di esercitazione dalla compilazione e quindi dal runtime in fase di runtime:

 public static void swap(List l1, List l2) { Number num = l1.get(0); l1.add(0, l2.get(0)); l2.add(0, num); } 

Sì, la specifica della lingua e il compilatore probabilmente potrebbero essere resi più sofisticati per distinguere il tuo esempio da quello, ma non lo è e è abbastanza semplice da aggirare.

Forse questo può spiegare il problema del compilatore:

 List myNums = new ArrayList(); 

Questo elenco di caratteri jolly generici può contenere tutti gli elementi che si estendono da Numero. Quindi è OK assegnare una lista intera ad essa. Comunque ora potrei aggiungere un Double a myNums perché Double si sta estendendo anche da Number, il che porterebbe a un problema di runtime. Quindi il compilatore proibisce ogni accesso in scrittura a myNums e io posso solo usare i metodi di lettura su di esso, perché so solo che cosa ottengo può essere lanciato su Numero.

E così il compilatore si lamenta di un sacco di cose che si possono fare con un generico di tali caratteri jolly. A volte è pazzo di cose che puoi garantire che siano sicure e OK.

Ma fortunatamente c’è un trucco per aggirare questo errore in modo da poter testare da solo ciò che può forse rompere questo:

 public static void main(String[] args) { List list1 = new ArrayList(); List> list2 = copyHelper(list1); } private static  List> copyHelper(List list) { return Collections.singletonList(list); } 

Il motivo è che il compilatore non sa che i tuoi tipi di caratteri jolly sono dello stesso tipo.

Inoltre, non sa che la tua istanza è null . Sebbene null sia membro di tutti i tipi, il compilatore considera solo i tipi dichiarati , non ciò che potrebbe contenere il valore della variabile, quando controlla il tipo.

Se il codice viene eseguito, non causerebbe un’eccezione, ma solo perché il valore è nullo. C’è ancora un potenziale disallineamento di tipo, ed è quello che il lavoro del compilatore è – per disabilitare le mancate corrispondenze di tipo.

Dai un’occhiata al tipo di cancellazione . Il problema è che il “tempo di compilazione” è l’unica opportunità che Java ha per applicare questi generici, quindi se lascia che ciò avvenga non sarebbe in grado di dire se si è tentato di inserire qualcosa di non valido. Questa è in realtà una buona cosa, perché significa che una volta che il programma viene compilato, i Generics non subiscono alcuna penalità sulle prestazioni in fase di esecuzione.

Proviamo a guardare il tuo esempio in un altro modo (usiamo due tipi che estendono Number ma si comportano in modo molto diverso). Considera il seguente programma:

 import java.math.BigDecimal; import java.util.*; public class q16449799 { public T val; public static void main(String ... args) { q16449799 t = new q16449799<>(); t.val = new BigDecimal(Math.PI); List> l = Collections.singletonList(t); for(q16449799 i : l) { System.out.println(i.val); } } } 

Queste uscite (come ci si aspetterebbe):

 3.141592653589793115997963468544185161590576171875 

Ora supponendo che il codice che hai presentato non abbia causato un errore del compilatore:

 import java.math.BigDecimal; import java.util.concurrent.atomic.AtomicLong; public class q16449799 { public T val; public static void main(String ... args) { q16449799 t = new q16449799<>(); t.val = new BigDecimal(Math.PI); List> l = Collections.singletonList(t); for(q16449799 i : l) { System.out.println(i.val); } } } 

Cosa ti aspetti dall’output? Non si può ragionevolmente trasmettere un BigDecimal ad un AtomicLong (si potrebbe build un AtomicLong dal valore di un BigDecimal, ma il casting e la costruzione sono cose diverse e i Generics sono implementati come lo zucchero del tempo di compilazione per assicurarsi che i lanci abbiano successo). Per quanto riguarda il commento di @KennyTM, un tipo concreto cerca nel tuo esempio iniziale ma prova a compilare questo:

 import java.math.BigDecimal; import java.util.*; public class q16449799 { public T val; public static void main(String ... args) { q16449799 t = new q16449799(); t.val = new BigDecimal(Math.PI); List> l = Collections.>singletonList(t); for(q16449799 i : l) { System.out.println(i.val); } } } 

Questo errore uscirà nel momento in cui t.val e imposterai un valore su t.val .