2009-05-22 6 views
149

Dato il seguente esempio (usando JUnit con matchers Hamcrest):Quando i generici Java richiedono anziché <T> e c'è qualche svantaggio della commutazione?

Map<String, Class<? extends Serializable>> expected = null; 
Map<String, Class<java.util.Date>> result = null; 
assertThat(result, is(expected)); 

Questo non viene compilato con il metodo JUnit assertThat firma:

public static <T> void assertThat(T actual, Matcher<T> matcher) 

Il messaggio di errore del compilatore è:

Error:Error:line (102)cannot find symbol method 
assertThat(java.util.Map<java.lang.String,java.lang.Class<java.util.Date>>, 
org.hamcrest.Matcher<java.util.Map<java.lang.String,java.lang.Class 
    <? extends java.io.Serializable>>>) 

Tuttavia, se cambio la firma del metodo assertThat in:

public static <T> void assertThat(T result, Matcher<? extends T> matcher) 

Quindi la compilazione funziona.

Quindi tre domande:

  1. Perché proprio non ha la versione corrente compilare? Anche se capisco vagamente i problemi di covarianza qui, non potrei certamente spiegarlo se dovessi farlo.
  2. C'è qualche svantaggio nel cambiare il metodo assertThat in Matcher<? extends T>? Ci sono altri casi che si interrompono se lo fai?
  3. C'è qualche punto per la generalizzazione del metodo assertThat in JUnit? La classe Matcher non sembra averlo richiesto, dal momento che JUnit chiama il metodo di corrispondenza, che non è stato digitato con alcun generico, e sembra proprio un tentativo di forzare un tipo di sicurezza che non fa nulla, poiché lo Matcher non sarà sufficiente in effetti corrispondono, e il test fallirà a prescindere. Non sono coinvolte operazioni pericolose (o almeno così sembra).

Per riferimento, ecco l'implementazione di JUnit assertThat:

public static <T> void assertThat(T actual, Matcher<T> matcher) { 
    assertThat("", actual, matcher); 
} 

public static <T> void assertThat(String reason, T actual, Matcher<T> matcher) { 
    if (!matcher.matches(actual)) { 
     Description description = new StringDescription(); 
     description.appendText(reason); 
     description.appendText("\nExpected: "); 
     matcher.describeTo(description); 
     description 
      .appendText("\n  got: ") 
      .appendValue(actual) 
      .appendText("\n"); 

     throw new java.lang.AssertionError(description.toString()); 
    } 
} 
+0

questo collegamento è molto utile (generici, ereditarietà e sottotipo): http://docs.oracle.com/javase/tutorial/java/generics/inheritance.html –

risposta

107

In primo luogo - devo indirizzarvi verso http://www.angelikalanger.com/GenericsFAQ/JavaGenericsFAQ.html - lei fa un lavoro incredibile.

L'idea di base è che si utilizza

<T extends SomeClass> 

quando il parametro attuale può essere SomeClass o qualsiasi sottotipo di esso.

Nel tuo esempio,

Map<String, Class<? extends Serializable>> expected = null; 
Map<String, Class<java.util.Date>> result = null; 
assertThat(result, is(expected)); 

stai dicendo che expected possono contenere oggetti di classe che rappresentano ogni classe che implementa Serializable. La tua mappa dei risultati dice che può contenere solo oggetti di classe Date.

Quando si passa nel risultato, si sta impostando T esattamente Map di String a Date oggetti di classe, che non corrispondono a Map di String a tutto ciò che è Serializable.

Una cosa da verificare - sei sicuro di volere Class<Date> e non Date?Una mappa di String-Class<Date> non suona terribilmente utile in generale (tutto ciò che può contenere è Date.class come valori, piuttosto che le istanze di Date)

Per quanto riguarda genericizing assertThat, l'idea è che il metodo può garantire che un Matcher che si adatta il tipo di risultato viene passato.

+0

In questo caso, sì, desidero una mappa di classi. L'esempio che ho dato è concepito per utilizzare classi JDK standard piuttosto che le mie classi personalizzate, ma in questo caso la classe viene in realtà creata tramite la reflection e utilizzata in base alla chiave. (Un'app distribuita in cui il client non ha le classi server disponibili, solo la chiave di quale classe utilizzare per fare funzionare il lato server). – Yishai

+5

Immagino che il mio cervello sia bloccato sul motivo per cui una mappa contenente classi di tipo Date non si adatti semplicemente a un tipo di Maps contenente classi di tipo Serializable. Certo, le classi di tipo Serializable potrebbero essere anche altre classi, ma certamente include il tipo Date. – Yishai

+0

AssertAssicurandosi che il cast sia eseguito per te, il metodo matcher.matches() non interessa, quindi dal momento che il T non viene mai usato, perché coinvolgerlo? (il metodo return type è nullo) – Yishai

8

il motivo per il codice originale non compila è che <? extends Serializable> fa non media "ogni classe che estende Serializable", ma "un po 'di classe sconosciuta, ma specifica che si estende Serializable."

Ad esempio, dato il codice come scritto, è perfettamente valido assegnare new TreeMap<String, Long.class>()> a expected. Se il compilatore consentiva la compilazione del codice, il valore assertThat() si interrompeva presumibilmente perché prevedeva gli oggetti Date invece degli oggetti Long che trova nella mappa.

+1

Non sto seguendo abbastanza - quando dici "non significa ... ma ...": qual è la differenza? (come, quale sarebbe un esempio di una classe "nota ma non specifica" che si adatta alla precedente definizione ma non alla seconda?) – poundifdef

+0

Sì, è un po 'imbarazzante; non so come esprimerlo meglio ... ha più senso dire "'?' è un tipo sconosciuto, non un tipo che corrisponde a qualcosa? " – erickson

+0

Ciò che può aiutare a spiegarlo è che per "qualsiasi classe che estende Serializable" si potrebbe semplicemente usare '' – c0der

5

Un modo per capire i caratteri jolly è pensare che il carattere jolly non stia specificando il tipo di possibili oggetti che un riferimento generico può avere, ma il tipo di altri riferimenti generici che è compatibile con (questo può sembrare confuso ...) Come tale, la prima risposta è molto fuorviante nella sua formulazione.

In altre parole, List<? extends Serializable> significa che è possibile assegnare tale riferimento ad altri elenchi in cui il tipo è un tipo sconosciuto che è o una sottoclasse di Serializable. NON pensarlo in termini di UNA LISTA UNICA in grado di contenere sottoclassi di Serializable (perché questa è semantica scorretta e porta a un fraintendimento di Generics).

+0

Ciò sicuramente aiuta, ma "può sembrare confuso" è una specie di "sostituzione confusa". Come seguito, quindi perché, in base a questa spiegazione, compila il metodo con Matcher' ? – Yishai

+0

se definiamo come Elenco , fa la stessa cosa giusto? Sto parlando del tuo secondo para. cioè il polimorfismo lo avrebbe gestito? –

10

Si riduce a:

Class<? extends Serializable> c1 = null; 
Class<java.util.Date> d1 = null; 
c1 = d1; // compiles 
d1 = c1; // wont compile - would require cast to Date 

Si può vedere la c1 di riferimento di classe potrebbe contenere un esempio lungo (dal momento che l'oggetto sottostante in un determinato momento avrebbe potuto essere List<Long>), ma ovviamente non può essere lanciato ad un Data poiché non vi è alcuna garanzia che la classe "sconosciuta" fosse Data. Non è tipicamente sicuro, quindi il compilatore non lo consente.

Tuttavia, se introduciamo qualche altro oggetto, ad esempio List (nel tuo esempio questo oggetto è Matcher), allora la seguente diventa vero:

List<Class<? extends Serializable>> l1 = null; 
List<Class<java.util.Date>> l2 = null; 
l1 = l2; // wont compile 
l2 = l1; // wont compile 

... Tuttavia, se il tipo di lista diventa ? estende T invece di T ....

List<? extends Class<? extends Serializable>> l1 = null; 
List<? extends Class<java.util.Date>> l2 = null; 
l1 = l2; // compiles 
l2 = l1; // won't compile 

Penso cambiando Matcher<T> to Matcher<? extends T>, si sono fondamentalmente introducendo lo scenario simile a assegnazione L1 = L2;

È ancora molto confuso l'uso di caratteri jolly nidificati, ma si spera che abbia senso spiegare perché aiuta a capire i farmaci generici, osservando come è possibile assegnare riferimenti generici l'uno all'altro. Inoltre confonde ulteriormente poiché il compilatore sta inferendo il tipo di T quando si effettua la chiamata alla funzione (non si sta dicendo esplicitamente che T è).

1

cosa succede se si utilizza

Map<String, ? extends Class<? extends Serializable>> expected = null; 
+0

Sì, questo è un po 'come la mia risposta di cui sopra stava guidando. – GreenieMeanie

+0

No, questo non aiuta la situazione, almeno nel modo in cui ho provato. – Yishai

21

Grazie a tutti coloro che hanno risposto alla domanda, in realtà aiutato a chiarire le cose per me. Alla fine la risposta di Scott Stanchfield si è avvicinata al modo in cui ho finito per capirlo, ma poiché non l'ho capito quando l'ha scritto per la prima volta, sto cercando di riaffermare il problema in modo che speriamo che qualcun altro possa trarne beneficio.

Ho intenzione di riformulare la domanda in termini di Elenco, dal momento che ha un solo parametro generico e che renderà più semplice la comprensione.

Lo scopo della classe parametrizzato (come ad esempio Lista <Date> o Mappa <K, V> come nell'esempio) è quello di forza un abbattuta e di avere la garanzia compilatore che questo è sicuro (senza eccezioni runtime).

Considerare il caso di Elenco. L'essenza della mia domanda è: perché un metodo che prende un tipo T e una lista non accetterà un elenco di qualcosa più in basso nella catena di ereditarietà di T. Si consideri questo esempio inventato:

List<java.util.Date> dateList = new ArrayList<java.util.Date>(); 
Serilizable s = new String(); 
addGeneric(s, dateList); 

.... 
private <T> void addGeneric(T element, List<T> list) { 
    list.add(element); 
} 

Questo non compilerà , perché il parametro list è un elenco di date, non un elenco di stringhe. Generics non sarebbe molto utile se fosse compilato.

La stessa cosa si applica a una mappa <String, Class<? extends Serializable>> Non è la stessa cosa di una mappa <String, Class<java.util.Date>>. Essi non sono covariante, quindi se ho voluto prendere un valore dalla mappa che contiene le classi di data e metterlo nella mappa che contiene elementi serializzabili, che va bene, ma una firma del metodo che dice:

private <T> void genericAdd(T value, List<T> list) 

vuole essere in grado di fare entrambe le cose:

T x = list.get(0); 

e

list.add(value); 

In questo caso, anche se il metodo JUnit in realtà non si preoccupa di queste cose, la firma del metodo richiede la covarianza, che non sta ottenendo, quindi non si compila.

riguarda la seconda domanda,

Matcher<? extends T> 

sarebbe il rovescio della medaglia in realtà accettare nulla quando T è un oggetto, che non è nelle intenzioni API. L'intento è di assicurare staticamente che il matcher corrisponda all'oggetto reale, e non c'è modo di escludere Object da quel calcolo.

La risposta alla terza domanda è che nulla andrebbe perso, in termini di funzionalità non controllata (non ci sarebbe alcun typecasting non sicuro all'interno dell'API JUnit se questo metodo non è stato generizzato), ma stanno cercando di realizzare qualcos'altro - assicurati staticamente che i due parametri possano corrispondere.

EDIT (dopo un'ulteriore contemplazione e l'esperienza):

Uno dei grossi problemi con il metodo assertThat firma è tentativi di equiparare una T variabile con un parametro generico di T. che non funziona, perché sono non covariante. Quindi, ad esempio, potresti avere un T che è un List<String> ma poi passare una corrispondenza che il compilatore risolve a Matcher<ArrayList<T>>. Ora se non fosse un parametro di tipo, le cose andrebbero bene, perché List e ArrayList sono covarianti, ma poiché Generics, per quanto riguarda il compilatore richiede ArrayList, non può tollerare un elenco per ragioni che spero siano chiare dall'alto.

+0

Non riesco ancora a capire perché non riesca a farlo. Perché non posso trasformare un elenco di date in un elenco serializzabile? –

+0

@ThomasAhle, perché i riferimenti che pensano che si tratti di un elenco di date verranno eseguiti in errori di trasmissione quando trovano le stringhe o qualsiasi altro serializable. – Yishai

+0

Vedo, ma cosa succede se in qualche modo mi sono liberato del vecchio riferimento, come se restituissi l''Elenco ' da un metodo con tipo 'Lista '? Questo dovrebbe essere sicuro, anche se non autorizzato da java. –

2

So che questa è una vecchia domanda, ma voglio condividere un esempio che, a mio avviso, spiega piuttosto bene i caratteri jolly limitati.java.util.Collections offre questo metodo:

public static <T> void sort(List<T> list, Comparator<? super T> c) { 
    list.sort(c); 
} 

Se abbiamo un elenco di T, l'elenco può, ovviamente, contengono istanze dei tipi che si estendono T. Se la lista contiene animali, la lista può contenere sia cani che gatti (entrambi animali). I cani hanno una proprietà "woofVolume" e i gatti hanno una proprietà "meowVolume". Mentre potremmo voler ordinare in base a queste proprietà in particolare alle sottoclassi di T, come possiamo aspettarci che questo metodo lo faccia? Una limitazione di Comparator è che può confrontare solo due cose di un solo tipo (T). Quindi, richiedendo semplicemente un Comparator<T> renderebbe questo metodo utilizzabile. Ma il creatore di questo metodo ha riconosciuto che se qualcosa è un T, allora è anche un'istanza delle superclassi di T. Pertanto, ci consente di utilizzare un comparatore di T o qualsiasi superclasse di T, ad esempio ? super T.