Estou um pouco confuso sobre como os genéricos Java lidam com herança / polimorfismo.
Suponha a seguinte hierarquia -
Animal (Pai)
Cão - Gato (Crianças)
Então, suponha que eu tenha um método doSomething(List<Animal> animals)
. Por todas as regras de herança e polimorfismo, eu assumiria que a List<Dog>
é a List<Animal>
e a List<Cat>
é a List<Animal>
- e, portanto, qualquer uma delas poderia ser passada para esse método. Não tão. Se eu quero atingir esse comportamento, preciso informar explicitamente o método para aceitar uma lista de qualquer subclasse de Animal dizendo doSomething(List<? extends Animal> animals)
.
Eu entendo que esse é o comportamento do Java. Minha pergunta é por que ? Por que o polimorfismo geralmente está implícito, mas quando se trata de genéricos, ele deve ser especificado?
fonte
Respostas:
Não, a não
List<Dog>
é a . Considere o que você pode fazer com um - você pode adicionar qualquer animal a ele ... incluindo um gato. Agora, você pode adicionar logicamente um gato a uma ninhada de filhotes? Absolutamente não.List<Animal>
List<Animal>
De repente você tem um gato muito confuso.
Agora, você não pode adicionar um
Cat
aList<? extends Animal>
porque não sabe que é umList<Cat>
. Você pode recuperar um valor e saber que será umAnimal
, mas não pode adicionar animais arbitrários. O inverso é verdadeiro paraList<? super Animal>
- nesse caso, você pode adicionar umAnimal
a ele com segurança, mas não sabe nada sobre o que pode ser recuperado, porque poderia ser umList<Object>
.fonte
O que você está procurando é chamado de parâmetros do tipo covariante . Isso significa que, se um tipo de objeto pode ser substituído por outro em um método (por exemplo,
Animal
pode ser substituído porDog
), o mesmo se aplica às expressões que usam esses objetos (portanto,List<Animal>
pode ser substituído porList<Dog>
). O problema é que a covariância não é segura para listas mutáveis em geral. Suponha que você tenha umList<Dog>
e esteja sendo usado como umList<Animal>
. O que acontece quando você tenta adicionar um gato a isso,List<Animal>
que é realmente umList<Dog>
? Permitir automaticamente que os parâmetros de tipo sejam covariantes interrompe o sistema de tipos.Seria útil adicionar sintaxe para permitir que parâmetros de tipo sejam especificados como covariantes, o que evita as
? extends Foo
declarações do método in, mas isso adiciona complexidade adicional.fonte
O motivo a
List<Dog>
não é aList<Animal>
, é que, por exemplo, você pode inserir aCat
em aList<Animal>
, mas não em umaList<Dog>
... você pode usar curingas para tornar os genéricos mais extensíveis sempre que possível; por exemplo, ler de aList<Dog>
é o mesmo que ler deList<Animal>
- mas não de escrever.Os genéricos na linguagem Java e a seção sobre genéricos dos tutoriais Java têm uma explicação muito boa e aprofundada sobre por que algumas coisas são ou não polimórficas ou permitidas com genéricos.
fonte
Um ponto que acho que deve ser adicionado ao que outras respostas mencionam é que, embora
também é verdade que
A maneira como a intuição do OP funciona - que é completamente válida, é claro - é a última frase. No entanto, se aplicarmos essa intuição, obtemos uma linguagem que não é do tipo Java em seu sistema de tipos: Suponha que nossa linguagem permita adicionar um gato à nossa lista de cães. O que isso significa? Isso significaria que a lista deixa de ser uma lista de cães e continua sendo apenas uma lista de animais. E uma lista de mamíferos e uma lista de quadrúpedes.
Em outras
List<Dog>
palavras : A em Java não significa "uma lista de cães" em inglês, significa "uma lista que pode ter cães e nada mais".De maneira mais geral, a intuição do OP se presta a uma linguagem na qual as operações nos objetos podem alterar seu tipo , ou melhor, o (s) tipo (s) de um objeto é uma função (dinâmica) de seu valor.
fonte
Eu diria que o ponto principal dos genéricos é que não permite isso. Considere a situação com matrizes, que permitem esse tipo de covariância:
Esse código compila bem, mas gera um erro de tempo de execução (
java.lang.ArrayStoreException: java.lang.Boolean
na segunda linha). Não é seguro. O objetivo dos genéricos é adicionar a segurança do tipo de tempo de compilação, caso contrário, você pode simplesmente continuar com uma classe simples sem genéricos.Agora há momentos em que você precisa ser mais flexível e que é o que o
? super Class
e? extends Class
são para. O primeiro é quando você precisa inserir um tipoCollection
(por exemplo), e o segundo é quando você precisa lê-lo, de maneira segura. Mas a única maneira de fazer as duas coisas ao mesmo tempo é ter um tipo específico.fonte
Para entender o problema, é útil fazer uma comparação com matrizes.
List<Dog>
não é subclasse deList<Animal>
.Mas
Dog[]
é subclasse deAnimal[]
.As matrizes são reificáveis e covariantes .
Reifiable significa que suas informações de tipo estão totalmente disponíveis em tempo de execução.
Portanto, as matrizes fornecem segurança do tipo tempo de execução, mas não do tipo tempo de compilação.
É vice-versa para os genéricos: os
genéricos são apagados e invariáveis .
Portanto, os genéricos não podem fornecer segurança de tipo de tempo de execução, mas fornecem segurança de tipo de tempo de compilação.
No código abaixo, se os genéricos forem covariantes, será possível causar poluição na pilha na linha 3.
fonte
As respostas dadas aqui não me convenceram completamente. Então, em vez disso, dou outro exemplo.
soa bem, não é? Mas você só pode passar
Consumer
s eSupplier
s porAnimal
s. Se você tem umMammal
consumidor, mas umDuck
fornecedor, eles não devem se encaixar, embora ambos sejam animais. Para não permitir isso, restrições adicionais foram adicionadas.Em vez do acima, temos que definir relações entre os tipos que usamos.
Por exemplo.,
garante que só podemos usar um fornecedor que nos forneça o tipo certo de objeto para o consumidor.
OTOH, nós também poderíamos
onde vamos para o outro lado: definimos o tipo de
Supplier
e restringimos que ele pode ser colocado noConsumer
.Nós podemos até fazer
onde, tendo as relações intuitivas
Life
->Animal
->Mammal
->Dog
,Cat
etc., poderíamos até mesmo colocar umMammal
em umLife
consumidor, mas nãoString
em umLife
consumidor.fonte
(Consumer<Runnable>, Supplier<Dog>)
whileDog
é subtipo deAnimal & Runnable
A lógica básica para esse comportamento é
Generics
seguir um mecanismo do tipo apagamento. Portanto, em tempo de execução, você não tem como identificar o tipocollection
diferente dearrays
onde não há esse processo de apagamento. Então, voltando à sua pergunta ...Então, suponha que exista um método conforme indicado abaixo:
Agora, se o java permitir que o chamador adicione a lista do tipo Animal a esse método, você pode adicionar algo errado à coleção e, no tempo de execução, ele também será executado devido ao apagamento do tipo. Enquanto no caso de matrizes, você receberá uma exceção em tempo de execução para esses cenários ...
Assim, em essência, esse comportamento é implementado para que não se possa adicionar algo errado à coleção. Agora acredito que o apagamento de tipo existe para dar compatibilidade com o legado java sem genéricos ....
fonte
A subtipagem é invariável para tipos parametrizados. Por mais difícil que a classe
Dog
seja um subtipo deAnimal
, o tipo parametrizadoList<Dog>
não é um subtipo deList<Animal>
. Por outro lado, a subtipo covariante é usada por matrizes, portanto, o tipo de matrizDog[]
é um subtipo deAnimal[]
.A subtipagem invariável garante que as restrições de tipo impostas pelo Java não sejam violadas. Considere o seguinte código fornecido por @Jon Skeet:
Conforme afirmado por Jon Skeet, esse código é ilegal, pois, caso contrário, violaria as restrições de tipo ao retornar um gato quando um cachorro o esperava.
É instrutivo comparar o código acima com o código análogo para matrizes.
O código é legal. No entanto, lança uma exceção de armazenamento de matriz . Uma matriz carrega seu tipo em tempo de execução, dessa maneira a JVM pode impor a segurança de tipo de subtipo coviante.
Para entender isso ainda mais, vejamos o bytecode gerado pela
javap
classe abaixo:Usando o comando
javap -c Demonstration
, isso mostra o seguinte bytecode Java:Observe que o código traduzido dos corpos dos métodos é idêntico. O compilador substituiu cada tipo parametrizado por sua exclusão . Essa propriedade é crucial, o que significa que não quebrou a compatibilidade com versões anteriores.
Em conclusão, a segurança em tempo de execução não é possível para tipos parametrizados, pois o compilador substitui cada tipo parametrizado por sua eliminação. Isso faz com que tipos parametrizados nada mais sejam do que açúcar sintático.
fonte
Na verdade, você pode usar uma interface para alcançar o que deseja.
}
você pode usar as coleções usando
fonte
Se você tem certeza de que os itens da lista são subclasses desse super tipo, você pode converter a lista usando esta abordagem:
Isso é útil quando você deseja passar a lista em um construtor ou iterar sobre ela
fonte
A resposta e outras respostas estão corretas. Vou acrescentar a essas respostas uma solução que acho que será útil. Eu acho que isso aparece frequentemente na programação. Um aspecto a ser observado é que, para coleções (listas, conjuntos, etc.), o principal problema é adicionar à coleção. É aí que as coisas desmoronam. Mesmo remover está OK.
Na maioria dos casos, podemos usá
Collection<? extends T>
-loCollection<T>
e essa deve ser a primeira escolha. No entanto, estou encontrando casos em que não é fácil fazer isso. Está em debate se essa é sempre a melhor coisa a se fazer. Estou apresentando aqui uma classe DownCastCollection que pode converter convert aCollection<? extends T>
aCollection<T>
(podemos definir classes semelhantes para List, Set, NavigableSet, ..) a serem usadas ao usar a abordagem padrão, sendo muito inconveniente. Abaixo está um exemplo de como usá-lo (também poderíamos usarCollection<? extends Object>
neste caso, mas estou simplificando a ilustração usando DownCastCollection.Agora a turma:
}
fonte
Collections.unmodifiableCollection
Collection<? extends E>
já lida com esse comportamento corretamente, a menos que você o use de uma maneira que não seja segura para o tipo (por exemplo, convertendo-o para outra coisa). A única vantagem que vejo é que, quando você chama aadd
operação, ela lança uma exceção, mesmo que você a tenha convertido.Vamos pegar o exemplo do tutorial do JavaSE
Portanto, por que uma lista de cães (círculos) não deve ser considerada implicitamente, uma lista de animais (formas) se deve a esta situação:
Então, os "arquitetos" Java tinham 2 opções que abordam esse problema:
não considere que um subtipo é implicitamente supertipo e dê um erro de compilação, como acontece agora
considere o subtipo como supertipo e restrinja ao compilar o método "add" (portanto, no método drawAll, se uma lista de círculos, subtipo de forma for aprovada, o compilador deve detectar isso e restringir você com erro de compilação aquele).
Por razões óbvias, essa foi a primeira maneira.
fonte
Também devemos levar em consideração como o compilador ameaça as classes genéricas: "instancia" um tipo diferente sempre que preenchermos os argumentos genéricos.
Assim, temos
ListOfAnimal
,ListOfDog
,ListOfCat
, etc, que são classes distintas que acabam sendo "criado" pelo compilador quando especificar os argumentos genéricos. E essa é uma hierarquia simples (na verdade, a respeito deList
não é uma hierarquia).Outro argumento pelo qual a covariância não faz sentido no caso de classes genéricas é o fato de que, na base, todas as classes são iguais - são
List
instâncias. A especialização de umList
preenchimento do argumento genérico não estende a classe, apenas faz funcionar para esse argumento genérico específico.fonte
O problema foi bem identificado. Mas há uma solução; tornar doSomething genérico:
agora você pode chamar doSomething com List <Dog> ou List <Cat> ou List <Animal>.
fonte
outra solução é criar uma nova lista
fonte
Além da resposta de Jon Skeet, que usa este código de exemplo:
No nível mais profundo, o problema aqui é esse
dogs
eanimals
compartilhe uma referência. Isso significa que uma maneira de fazer esse trabalho seria copiar a lista inteira, o que quebraria a igualdade de referência:Após ligar
List<Animal> animals = new ArrayList<>(dogs);
, você não pode atribuir diretamente diretamenteanimals
a umdogs
ou acats
:portanto, você não pode colocar o subtipo errado de
Animal
na lista, porque não há subtipo errado - qualquer objeto do subtipo? extends Animal
pode ser adicionadoanimals
.Obviamente, isso muda a semântica, uma vez que as listas
animals
edogs
deixam de ser partilhadas, assim que adicionar a uma lista não adicionar para o outro (que é exatamente o que você quer, para evitar o problema de que umCat
pode ser adicionado a uma lista que só é deveria conterDog
objetos). Além disso, copiar a lista inteira pode ser ineficiente. No entanto, isso resolve o problema de equivalência de tipo, quebrando a igualdade de referência.fonte