Quando i generici Java richiedono <? estende T> invece di <T> e c'è qualche svantaggio di commutazione?


205

Dato il seguente esempio (utilizzando JUnit con i corrispondenti Hamcrest):

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

Questo non viene compilato con la assertThatfirma del metodo JUnit di:

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 assertThatfirma del metodo in:

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

Quindi la compilazione funziona.

Quindi tre domande:

  1. Perché esattamente la versione attuale non viene compilata? Anche se capisco vagamente le questioni di covarianza qui, non potrei certamente spiegarlo se dovessi.
  2. C'è qualche svantaggio nel cambiare il assertThatmetodo in Matcher<? extends T>? Ci sono altri casi che potrebbero rompersi se lo facessi?
  3. C'è qualche punto per la generalizzazione del assertThatmetodo in JUnit? La Matcherclasse non sembra richiederlo, dal momento che JUnit chiama il metodo match, che non viene digitato con alcun generico, e sembra solo un tentativo di forzare un tipo di sicurezza che non fa nulla, poiché la Matchervolontà non lo fa partita e il test fallirà a prescindere. Nessuna operazione pericolosa coinvolta (o almeno così sembra).

Per riferimento, ecco l'implementazione JUnit di 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());
    }
}

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

Risposte:


145

Innanzitutto, devo indirizzarti a http://www.angelikalanger.com/GenericsFAQ/JavaGenericsFAQ.html - lei fa un lavoro fantastico.

L'idea di base è che usi

<T extends SomeClass>

quando il parametro reale può essere SomeClasso 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 expectedpuò contenere oggetti Class che rappresentano qualsiasi classe che implementa Serializable. La mappa dei risultati dice che può contenere solo Dateoggetti di classe.

Quando si passa nel risultato, si sta impostando Tesattamente Mapdi Stringper Dateoggetti di classe, che non corrispondono a Mapdei Stringper tutto ciò che è Serializable.

Una cosa da controllare: sei sicuro di volerlo Class<Date>e no Date? Una mappa di Stringto Class<Date>non sembra terribilmente utile in generale (tutto ciò che può contenere è Date.classcome valori anziché come istanze di Date)

Per quanto riguarda la generalizzazione assertThat, l'idea è che il metodo possa garantire che Matchervenga passato un tipo adatto al tipo di risultato.


In questo caso, sì, voglio una mappa delle classi. L'esempio che ho fornito è concepito per utilizzare le classi JDK standard anziché le mie classi personalizzate, ma in questo caso la classe viene effettivamente istanziata tramite 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 il lato server).
Yishai,

6
Suppongo che il mio cervello sia bloccato perché una Mappa contenente classi di tipo Date non si adatta perfettamente a un tipo di Mappe che contengono classi di tipo Serializable. Sicuramente le classi di tipo serializzabili potrebbero essere anche altre classi, ma include certamente il tipo Date.
Yishai,

Sull'assertThat assicurandosi che il cast sia eseguito per te, al metodo matcher.matches () non importa, quindi poiché la T non viene mai usata, perché coinvolgerla? (il tipo restituito dal metodo è nullo)
Yishai

Ahhh - questo è quello che ottengo per non aver letto la definizione dell'asserzione abbastanza vicino. Sembra solo per garantire che un Matcher adatto sia passato in ...
Scott Stanchfield,

28

Grazie a tutti coloro che hanno risposto alla domanda, mi ha davvero aiutato a chiarire le cose per me. Alla fine la risposta di Scott Stanchfield è diventata la più vicina a come ho finito per capirla, ma dal momento che non l'ho capito quando l'ha scritta per la prima volta, sto cercando di riaffermare il problema in modo che qualcuno possa trarne beneficio.

Ho intenzione di riaffermare la domanda in termini di Elenco, poiché ha solo un parametro generico e ciò renderà più facile la comprensione.

Lo scopo della classe parametrizzata (come Elenco <Date>o Mappa <K, V>come nell'esempio) è forzare un downcast e far sì che il compilatore garantisca che ciò sia sicuro (senza eccezioni di runtime).

Considera il caso di List. L'essenza della mia domanda è perché un metodo che prende un tipo T e una Lista non accetterà una Lista di qualcosa più in basso della catena dell'eredità rispetto a T. Considera questo esempio inventato:

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

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

Questo non verrà compilato, poiché il parametro list è un elenco di date, non un elenco di stringhe. I generici non sarebbero molto utili se questo fosse compilato.

La stessa cosa vale per una mappa <String, Class<? extends Serializable>>Non è la stessa cosa di una mappa <String, Class<java.util.Date>>. Non sono covarianti, quindi se volevo prendere un valore dalla mappa contenente classi di date e inserirlo nella mappa contenente elementi serializzabili, 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 viene compilata.

Sulla seconda domanda,

Matcher<? extends T>

Avrebbe il rovescio della medaglia di accettare davvero qualsiasi cosa quando T è un oggetto, che non è l'intento delle API. L'intento è quello di garantire staticamente che il matcher corrisponda all'oggetto reale e non c'è modo di escludere l'oggetto da quel calcolo.

La risposta alla terza domanda è che nulla andrebbe perso, in termini di funzionalità non selezionata (non ci sarebbe un typecasting non sicuro all'interno dell'API JUnit se questo metodo non fosse stato generalizzato), ma stanno cercando di realizzare qualcos'altro - assicurati staticamente che il è probabile che due parametri corrispondano.

MODIFICA (dopo ulteriori riflessioni ed esperienze):

Uno dei maggiori problemi con l'asserzione che la firma del metodo sono i tentativi di equiparare una variabile T a un parametro generico di T. Non funziona, perché non sono covarianti. Quindi, ad esempio, potresti avere una T che è una List<String>ma poi passare una corrispondenza a cui il compilatore risolve 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 motivi che spero siano chiari dall'alto.


Tuttavia non capisco perché non riesco ad effettuare l'upgrade. Perché non riesco a trasformare un elenco di date in un elenco di numeri serializzabili?
Thomas Ahle,

@ThomasAhle, perché quindi i riferimenti che pensano che sia un elenco di date si imbatteranno in errori di casting quando trovano stringhe o altri serializzabili.
Yishai,

Capisco, ma cosa succede se in qualche modo mi sbarazzassi del vecchio riferimento, come se restituissi il List<Date>metodo da un tipo List<Object>? Questo dovrebbe essere sicuro, anche se non consentito da Java.
Thomas Ahle,

14

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

Puoi vedere che il riferimento di classe c1 potrebbe contenere un'istanza Long (dato che l'oggetto sottostante in un determinato momento avrebbe potuto essere List<Long>), ma ovviamente non è possibile eseguire il cast su una Data poiché non vi è alcuna garanzia che la classe "sconosciuta" fosse Date. Non è typsesafe, quindi il compilatore non lo consente.

Tuttavia, se introduciamo qualche altro oggetto, diciamo Elenco (nel tuo esempio questo oggetto è Matcher), allora diventa vero quanto segue:

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 Elenco 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 che cambiando Matcher<T> to Matcher<? extends T>, stai sostanzialmente introducendo uno scenario simile all'assegnazione di l1 = l2;

È ancora molto confuso avere caratteri jolly nidificati, ma si spera che abbia senso il motivo per cui aiuta a capire i generici osservando come è possibile assegnare riferimenti generici tra loro. È anche più confuso poiché il compilatore sta inferendo il tipo di T quando si effettua la chiamata di funzione (non si dice esplicitamente che T è).


9

La ragione per cui il codice originale non lo fa di compilazione è che <? extends Serializable>fa non media "ogni classe che estende Serializable", ma "un po 'sconosciuto, ma specifica classe che estende Serializable."

Ad esempio, dato il codice come scritto, è perfettamente valido da assegnare new TreeMap<String, Long.class>()>a expected. Se il compilatore consentisse la compilazione del codice, assertThat()si presumibilmente si spezzerebbe perché si aspetterebbe Dateoggetti invece degli Longoggetti che trova nella mappa.


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

Sì, è un po 'imbarazzante; non sono sicuro di come esprimerlo meglio ... ha più senso dire ""? " è un tipo sconosciuto, non un tipo che corrisponde a qualcosa? "
Erickson,

1
Ciò che può aiutare a spiegarlo è che per "qualsiasi classe che estende Serializable" potresti semplicemente usare<Serializable>
c0der

8

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

In altre parole, List<? extends Serializable>significa che puoi assegnare quel riferimento ad altri Elenchi in cui il tipo è un tipo sconosciuto che è o una sottoclasse di Serializable. NON pensarci in termini di ELENCO UNICO in grado di contenere sottoclassi di serializzabili (perché questa è una semantica errata e porta a un malinteso su Generics).


Questo certamente aiuta, ma il "può sembrare confuso" è un po 'sostituito con un "sembra confuso". Come follow-up, quindi perché, secondo questa spiegazione, viene <? extends T>compilato il metodo con Matcher ?
Yishai,

se definiamo Lista <Serializable>, fa la stessa cosa, giusto? Sto parlando del tuo secondo paragrafo. cioè il polimorfismo lo gestirà?
Supun Wijerathne,

3

So che questa è una vecchia domanda, ma voglio condividere un esempio che penso spieghi abbastanza bene i caratteri jolly delimitati. java.util.Collectionsoffre 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 contenere istanze di tipi che si estendono T. Se l'elenco contiene animali, l'elenco 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à particolari delle 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, richiedere semplicemente a Comparator<T>renderebbe utilizzabile questo metodo. 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 To qualsiasi superclasse di T, ad es ? super T.


1

cosa succede se si utilizza

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

Sì, questo è un po 'quello a cui la mia risposta sopra stava guidando.
GreenieMeanie,

No, questo non aiuta la situazione, almeno nel modo in cui ho provato.
Yishai,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.