Quando os genéricos Java requerem <? estende T> em vez de <T> e há alguma desvantagem de alternar?

205

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 assertThatassinatura 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 assertThatassinatura 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:

  1. 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.
  2. Existe alguma desvantagem na alteração do assertThatmétodo para Matcher<? extends T>? Existem outros casos que quebrariam se você fizesse isso?
  3. Existe algum ponto para a generalização do assertThatmétodo no JUnit? A Matcherclasse 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 a Matchervontade 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());
    }
}
Yishai
fonte
este link é muito útil (genéricos, herança e subtipo): docs.oracle.com/javase/tutorial/java/generics/inheritance.html
Dariush Jafari

Respostas:

145

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

<T extends SomeClass>

quando o parâmetro real pode ser SomeClassou qualquer subtipo dele.

No seu exemplo,

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

Você está dizendo que expectedpode conter objetos de classe que representam qualquer classe que implementa Serializable. Seu mapa de resultados diz que só pode conterDate objetos de classe.

Quando você passar no resultado, você está definindo Ta exatamente Mapde Stringque Dateobjetos de classe, que não correspondem Mapde Stringque qualquer coisa que seja Serializable.

Uma coisa a verificar - você tem certeza que deseja Class<Date>e não Date? Um mapa de Stringpara Class<Date>não parece muito útil em geral (tudo o que pode conter é Date.classcomo valores e não como instâncias Date)

Quanto à genérica assertThat, a idéia é que o método possa garantir que um Matcherque se encaixe no tipo de resultado seja passado.

Scott Stanchfield
fonte
Nesse caso, sim, eu quero um mapa de classes. O exemplo que dei foi inventado para usar classes JDK padrão em vez de minhas classes personalizadas, mas nesse caso a classe é realmente instanciada por reflexão e usada com base na chave. (Um aplicativo distribuído no qual o cliente não possui as classes de servidor disponíveis, apenas a chave de qual classe usar para fazer o trabalho do lado do servidor).
Yishai
6
Eu acho que onde meu cérebro está preso é por que um mapa contendo classes do tipo Date não se encaixa muito bem em um tipo de mapas que contém classes do tipo Serializable. Claro que as classes do tipo Serializable também podem ser outras, mas certamente inclui o tipo Data.
Yishai
Ao garantir que o elenco seja realizado para você, o método matcher.matches () não se importa, portanto, como o T nunca é usado, por que envolvê-lo? (o tipo de método de retorno é void)
Yishai
Ahhh - é o que recebo por não ler a definição da afirmação. Parece que é apenas para garantir que um Matcher montagem é passado ...
Scott Stanchfield
28

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:

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);
}

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:

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

Quer poder fazer as duas coisas:

T x = list.get(0);

e

list.add(value);

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,

Matcher<? extends T>

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 realiza Matcher<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.

Yishai
fonte
Ainda não entendo por que não posso fazer upcast. Por que não consigo transformar uma lista de datas em uma lista de serializáveis?
Thomas Ahle
@ThomasAhle, porque as referências que pensam que é uma lista de datas sofrerão erros de conversão quando encontrarem Strings ou quaisquer outros serializáveis.
Yishai
Entendo, mas e se, de alguma maneira, eu me livrasse da referência antiga, como se eu retornasse o List<Date>de um método com o tipo List<Object>? Isso deve ser seguro, mesmo que não seja permitido pelo java.
Thomas Ahle
14

Tudo se resume a:

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

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:

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

... No entanto, se o tipo da lista se tornar? estende T em vez de T ....

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

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).

GreenieMeanie
fonte
9

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>()>a expected. Se o compilador permitisse que o código assertThat()fosse compilado, presumivelmente seria interrompido porque esperaria Dateobjetos em vez dos Longobjetos encontrados no mapa.

erickson
fonte
1
Não estou entendendo bem - quando você diz "não significa ... mas ...": qual é a diferença? (como, o que seria um exemplo de uma classe "conhecido, mas não específica", que se encaixa na definição anterior, mas não o último?)
poundifdef
Sim, isso é um pouco estranho; não sei como expressá-lo melhor ... faz mais sentido dizer ""? é um tipo desconhecido, não corresponde a nada? "
22611 erickson
1
O que pode ajudar a explicar isso é que, para "qualquer classe que estende Serializable" você poderia simplesmente usar<Serializable>
c0der
8

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).

GreenieMeanie
fonte
Isso certamente ajuda, mas o "pode ​​parecer confuso" é meio que substituído por um "parece confuso". Como acompanhamento, por que, de acordo com esta explicação, o método com o Matcher é <? extends T>compilado?
Yishai
se definirmos como Lista <Serializable>, faz o mesmo, certo? Estou falando do seu segundo parágrafo. ou seja, o polimorfismo lidaria com isso?
Supun Wijerathne
3

Sei que essa é uma pergunta antiga, mas quero compartilhar um exemplo que acho que explica muito bem os curingas limitados. java.util.Collectionsoferece este método:

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

Se tivermos uma lista de T, a lista pode, é claro, conter instâncias de tipos que se estendem T. 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 de T, 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 um Comparator<T>tornaria esse método utilizável. Mas, o criador desse método reconheceu que se algo é a T, também é uma instância das superclasses de T. Portanto, ele nos permite usar um comparador Tou qualquer superclasse de T, ie ? super T.

Lucas Ross
fonte
1

e se você usar

Map<String, ? extends Class<? extends Serializable>> expected = null;
newacct
fonte
Sim, esse é o tipo de resposta da minha resposta acima.
GreenieMeanie
Não, isso não ajuda a situação, pelo menos da maneira que tentei.
Yishai