Dado o seguinte exemplo (usando o JUnit com os correspondentes do Hamcrest):
Map<String, Class<? extends Serializable>> expected = null;
Map<String, Class<java.util.Date>> result = null;
assertThat(result, is(expected));
Isso não é compilado com a assertThat
assinatura do método JUnit de:
public static <T> void assertThat(T actual, Matcher<T> matcher)
A mensagem de erro do compilador é:
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>>>)
No entanto, se eu alterar a assertThat
assinatura do método para:
public static <T> void assertThat(T result, Matcher<? extends T> matcher)
Então a compilação funciona.
Então, três perguntas:
- Por que exatamente a versão atual não é compilada? Embora eu entenda vagamente as questões de covariância aqui, certamente não conseguiria explicar se fosse necessário.
- Existe alguma desvantagem na alteração do
assertThat
método paraMatcher<? extends T>
? Existem outros casos que quebrariam se você fizesse isso? - Existe algum ponto para a generalização do
assertThat
método no JUnit? AMatcher
classe parece não exigir isso, já que o JUnit chama o método de correspondências, que não é digitado com nenhum genérico, e parece uma tentativa de forçar uma segurança de tipo que não faz nada, pois aMatcher
vontade simplesmente não corresponder e o teste falhará independentemente. Nenhuma operação insegura envolvida (ou assim parece).
Para referência, aqui está a implementação JUnit de 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());
}
}
Respostas:
Primeiro - eu tenho que direcioná-lo para http://www.angelikalanger.com/GenericsFAQ/JavaGenericsFAQ.html - ela faz um trabalho incrível.
A ideia básica é que você use
quando o parâmetro real pode ser
SomeClass
ou qualquer subtipo dele.No seu exemplo,
Você está dizendo que
expected
pode conter objetos de classe que representam qualquer classe que implementaSerializable
. Seu mapa de resultados diz que só pode conterDate
objetos de classe.Quando você passar no resultado, você está definindo
T
a exatamenteMap
deString
queDate
objetos de classe, que não correspondemMap
deString
que qualquer coisa que sejaSerializable
.Uma coisa a verificar - você tem certeza que deseja
Class<Date>
e nãoDate
? Um mapa deString
paraClass<Date>
não parece muito útil em geral (tudo o que pode conter éDate.class
como valores e não como instânciasDate
)Quanto à genérica
assertThat
, a idéia é que o método possa garantir que umMatcher
que se encaixe no tipo de resultado seja passado.fonte
Obrigado a todos que responderam à pergunta, isso realmente ajudou a esclarecer as coisas para mim. No final, a resposta de Scott Stanchfield chegou mais perto de como eu a entendi, mas como não o entendi quando ele a escreveu pela primeira vez, estou tentando reafirmar o problema para que, com sorte, alguém se beneficie.
Vou reexaminar a questão em termos de Lista, pois ela possui apenas um parâmetro genérico e isso facilitará o entendimento.
O objetivo da classe parametrizada (como List
<Date>
ou Map,<K, V>
como no exemplo) é forçar um downcast e garantir que o compilador seja seguro (sem exceções de tempo de execução).Considere o caso da lista. A essência da minha pergunta é por que um método que usa um tipo T e uma lista não aceita uma lista de algo mais abaixo da cadeia de herança que T. Considere este exemplo artificial:
Isso não será compilado, porque o parâmetro list é uma lista de datas, não uma lista de strings. Os genéricos não seriam muito úteis se isso fosse compilado.
O mesmo se aplica a um mapa.
<String, Class<? extends Serializable>>
Não é a mesma coisa que um mapa<String, Class<java.util.Date>>
. Eles não são covariantes, portanto, se eu quisesse pegar um valor do mapa contendo classes de data e colocá-lo no mapa contendo elementos serializáveis, tudo bem, mas uma assinatura de método que diz:Quer poder fazer as duas coisas:
e
Nesse caso, mesmo que o método junit não se importe com essas coisas, a assinatura do método requer a covariância, que não está sendo obtida, portanto, não é compilada.
Na segunda questão,
Teria a desvantagem de realmente aceitar qualquer coisa quando T for um Objeto, que não é a intenção das APIs. A intenção é garantir estaticamente que o correspondente corresponda ao objeto real e não há como excluir o objeto desse cálculo.
A resposta para a terceira pergunta é que nada seria perdido, em termos de funcionalidade não verificada (não haveria conversão de tipo insegura na API JUnit se esse método não fosse genérico), mas eles estão tentando realizar outra coisa - garantir estaticamente que o é provável que dois parâmetros correspondam.
EDIT (após mais contemplação e experiência):
Um dos grandes problemas com a assinatura do método assertThat é tentar igualar uma variável T com um parâmetro genérico de T. Isso não funciona, porque eles não são covariantes. Por exemplo, você pode ter um T que é um,
List<String>
mas depois passar uma correspondência que o compilador realizaMatcher<ArrayList<T>>
. Agora, se não fosse um parâmetro de tipo, tudo ficaria bem, porque List e ArrayList são covariantes, mas como os Generics, no que diz respeito ao compilador, exigem ArrayList, ele não pode tolerar uma Lista por razões que espero que sejam claras. de cima.fonte
List<Date>
de um método com o tipoList<Object>
? Isso deve ser seguro, mesmo que não seja permitido pelo java.Tudo se resume a:
Você pode ver que a referência de classe c1 pode conter uma instância Long (já que o objeto subjacente em um determinado momento poderia ter sido
List<Long>
), mas obviamente não pode ser convertido para uma Data, pois não há garantia de que a classe "desconhecida" seja Data. Não é typsesafe, portanto, o compilador não o permite.No entanto, se introduzirmos outro objeto, digamos List (no seu exemplo, esse objeto é Matcher), o seguinte se tornará verdadeiro:
... No entanto, se o tipo da lista se tornar? estende T em vez de T ....
Eu acho que mudando
Matcher<T> to Matcher<? extends T>
, você está basicamente introduzindo o cenário semelhante à atribuição de l1 = l2;Ainda é muito confuso ter curingas aninhados, mas espero que faça sentido o motivo pelo qual ajuda a entender os genéricos examinando como é possível atribuir referências genéricas entre si. Também é mais confuso, pois o compilador está deduzindo o tipo de T quando você faz a chamada de função (você não está dizendo explicitamente que era T).
fonte
A razão pela qual seu código original não compila é que
<? extends Serializable>
isso não significa "qualquer classe que estenda Serializable", mas "alguma classe desconhecida, mas específica, que estenda Serializable".Por exemplo, dado o código como escrito, é perfeitamente válido atribuir
new TreeMap<String, Long.class>()>
aexpected
. Se o compilador permitisse que o códigoassertThat()
fosse compilado, presumivelmente seria interrompido porque esperariaDate
objetos em vez dosLong
objetos encontrados no mapa.fonte
<Serializable>
Uma maneira de entender os curingas é pensar que o curinga não está especificando o tipo de objetos possíveis que uma referência genérica pode "ter", mas o tipo de outras referências genéricas com as quais é compatível é compatível (isso pode parecer confuso ...) Como tal, a primeira resposta é muito enganadora na sua formulação.
Em outras palavras,
List<? extends Serializable>
significa que você pode atribuir essa referência a outras Listas em que o tipo é algum tipo desconhecido que é ou uma subclasse de Serializable. NÃO pense nisso em termos de UMA ÚNICA LISTA ser capaz de conter subclasses de Serializable (porque isso é semântica incorreta e leva a um mal-entendido sobre Genéricos).fonte
<? extends T>
compilado?Sei que essa é uma pergunta antiga, mas quero compartilhar um exemplo que acho que explica muito bem os curingas limitados.
java.util.Collections
oferece este método:Se tivermos uma lista de
T
, a lista pode, é claro, conter instâncias de tipos que se estendemT
. Se a lista contiver animais, a lista poderá conter cães e gatos (ambos os animais). Cães têm uma propriedade "woofVolume" e Gatos têm uma propriedade "meowVolume". Embora possamos classificar com base nessas propriedades específicas das subclasses deT
, como podemos esperar que esse método faça isso? Uma limitação do Comparator é que ele pode comparar apenas duas coisas de apenas um tipo (T
). Portanto, exigir apenas umComparator<T>
tornaria esse método utilizável. Mas, o criador desse método reconheceu que se algo é aT
, também é uma instância das superclasses deT
. Portanto, ele nos permite usar um comparadorT
ou qualquer superclasse deT
, ie? super T
.fonte
e se você usar
fonte