List <Dog> é uma subclasse da List <Animal>? Por que os genéricos Java não são implicitamente polimórficos?

770

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?

froadie
fonte
17
E uma pergunta gramatical totalmente independente que está me incomodando agora - meu título deveria ser "por que não são genéricos de Java" ou "por que não são genéricos de Java"? "Genéricos" é plural por causa do s ou singular porque é uma entidade?
precisa saber é o seguinte
25
Os genéricos, como são feitos em Java, são uma forma muito pobre de polimorfismo paramétrico. Não confie muito neles (como eu costumava fazer), porque um dia você atingirá suas limitações patéticas: o cirurgião estende o KABOOM do Handable <Scalpel>, do Handable <Sponge> ! Será que não computar [TM]. Existe a sua limitação de genéricos Java. Qualquer OOA / OOD pode ser traduzido corretamente para Java (e o MI pode ser feito muito bem usando interfaces Java), mas os genéricos simplesmente não são suficientes. Eles são bons para "coleções" e programação procedural que disseram (que é o que a maioria dos programadores Java faz de qualquer maneira ...).
precisa saber é o seguinte
8
A superclasse da lista <Dog> não é a lista <Animal>, mas a lista <?> (Ou seja, lista de tipo desconhecido). Genéricos apaga informações de tipo no código compilado. Isso é feito para que o código que está usando genéricos (java 5 e superior) seja compatível com versões anteriores do java sem genéricos.
Rai.skumar 04/12/12
9
@roadroadie, já que ninguém parecia responder ... definitivamente deveria ser "por que os genéricos de Java não são ...". A outra questão é que "genérico" é na verdade um adjetivo e, portanto, "genéricos" está se referindo a um substantivo no plural modificado por "genérico". Você poderia dizer "essa função é genérica", mas isso seria mais complicado do que dizer "essa função é genérica". No entanto, é um pouco complicado dizer "Java possui funções e classes genéricas", em vez de apenas "Java possui genéricos". Como alguém que escreveu a tese de mestrado sobre adjetivos, acho que você encontrou uma pergunta muito interessante!
dantiston

Respostas:

916

Não, a nãoList<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>

// Illegal code - because otherwise life would be Bad
List<Dog> dogs = new ArrayList<Dog>(); // ArrayList implements List
List<Animal> animals = dogs; // Awooga awooga
animals.add(new Cat());
Dog dog = dogs.get(0); // This should be safe, right?

De repente você tem um gato muito confuso.

Agora, você não pode adicionar um Cata List<? extends Animal>porque não sabe que é um List<Cat>. Você pode recuperar um valor e saber que será um Animal, mas não pode adicionar animais arbitrários. O inverso é verdadeiro para List<? super Animal>- nesse caso, você pode adicionar um Animala ele com segurança, mas não sabe nada sobre o que pode ser recuperado, porque poderia ser um List<Object>.

Jon Skeet
fonte
50
Curiosamente, toda lista de cães é realmente uma lista de animais, assim como a intuição nos diz. O ponto é que nem toda lista de animais é uma lista de cães; portanto, a mutação da lista adicionando um gato é o problema.
Ingo
66
@ Ingo: Não, na verdade não: você pode adicionar um gato a uma lista de animais, mas não pode adicionar um gato a uma lista de cães. Uma lista de cães é apenas uma lista de animais se você a considerar em um modo somente leitura.
Jon Skeet
12
@ JonSkeet - Claro, mas quem está mandando que fazer uma nova lista de um gato e uma lista de cães realmente mude a lista de cães? Esta é uma decisão de implementação arbitrária em Java. Um que vai contra a lógica e a intuição.
Ingo 28/01
7
@ Ingo: Eu não teria usado isso "certamente" para começar. Se você tem uma lista que diz no topo "Hotéis para onde queremos ir" e alguém adicionou uma piscina a ela, você acha isso válido? Não - é uma lista de hotéis, que não é uma lista de edifícios. E não é como se eu tivesse dito "Uma lista de cães não é uma lista de animais" - coloquei em termos de código , em uma fonte de código. Realmente não acho que exista ambiguidade aqui. O uso da subclasse estaria incorreto de qualquer maneira - é sobre compatibilidade de atribuição, não subclassificação.
precisa saber é o seguinte
14
@ruakh: O problema é que você está pagando no tempo de execução algo que pode ser bloqueado no tempo de compilação. E eu argumentaria que a covariância de array foi um erro de design para começar.
Jon Skeet
84

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, Animalpode ser substituído por Dog), o mesmo se aplica às expressões que usam esses objetos (portanto, List<Animal>pode ser substituído por List<Dog>). O problema é que a covariância não é segura para listas mutáveis ​​em geral. Suponha que você tenha um List<Dog>e esteja sendo usado como um List<Animal>. O que acontece quando você tenta adicionar um gato a isso, List<Animal>que é realmente um List<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 Foodeclarações do método in, mas isso adiciona complexidade adicional.

Michael Ekstrand
fonte
44

O motivo a List<Dog>não é a List<Animal>, é que, por exemplo, você pode inserir a Catem a List<Animal>, mas não em uma List<Dog>... você pode usar curingas para tornar os genéricos mais extensíveis sempre que possível; por exemplo, ler de a List<Dog>é o mesmo que ler de List<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.

Michael Aaron Safyan
fonte
36

Um ponto que acho que deve ser adicionado ao que outras respostas mencionam é que, embora

List<Dog>não é um List<Animal> em Java

também é verdade que

Uma lista de cães é uma lista de animais em inglês (bem, sob uma interpretação razoável)

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.

einpoklum
fonte
Sim, a linguagem humana é mais confusa. Mas ainda assim, depois de adicionar um animal diferente à lista de cães, ele ainda é uma lista de animais, mas não é mais uma lista de cães. A diferença é que um humano, com a lógica difusa, geralmente não tem problemas para perceber isso.
Vlasec #
Como alguém que acha as comparações constantes de matrizes ainda mais confusas, essa resposta o acertou em cheio. Meu problema foi a intuição da linguagem.
FLonLon 24/02/19
32

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:

  Object[] objects = new String[10];
  objects[0] = Boolean.FALSE;

Esse código compila bem, mas gera um erro de tempo de execução ( java.lang.ArrayStoreException: java.lang.Booleanna 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 Classe ? extends Classsão para. O primeiro é quando você precisa inserir um tipo Collection(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.

Yishai
fonte
13
Indiscutivelmente, a covariância de array é um erro de design de linguagem. Observe que, devido ao apagamento do tipo, tecnicamente o mesmo comportamento é impossível para a coleta genérica.
precisa
" Eu diria que o ponto principal dos genéricos é que não permite isso ". Você nunca pode ter certeza: Java e sistemas do tipo de Scala são Unsound: A Crise Existencial de nulo Ponteiros (apresentado na OOPSLA 2016) (desde corrigido parece)
David Tonhofer
15

Para entender o problema, é útil fazer uma comparação com matrizes.

List<Dog>não é subclasse de List<Animal>.
Mas Dog[] é subclasse de Animal[].

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.

    // All compiles but throws ArrayStoreException at runtime at last line
    Dog[] dogs = new Dog[10];
    Animal[] animals = dogs; // compiles
    animals[0] = new Cat(); // throws ArrayStoreException at runtime

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

    List<Dog> dogs = new ArrayList<>();
    List<Animal> animals = dogs; // compile-time error, otherwise heap pollution
    animals.add(new Cat());
outdev
fonte
2
Pode-se argumentar que, precisamente por isso, arrays em Java são quebradas ,
leonbloy
Matrizes sendo covariantes são um "recurso" do compilador.
Cristik
6

As respostas dadas aqui não me convenceram completamente. Então, em vez disso, dou outro exemplo.

public void passOn(Consumer<Animal> consumer, Supplier<Animal> supplier) {
    consumer.accept(supplier.get());
}

soa bem, não é? Mas você só pode passar Consumers e Suppliers por Animals. Se você tem um Mammalconsumidor, mas um Duckfornecedor, 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.,

public <A extends Animal> void passOn(Consumer<A> consumer, Supplier<? extends A> supplier) {
    consumer.accept(supplier.get());
}

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

public <A extends Animal> void passOn(Consumer<? super A> consumer, Supplier<A> supplier) {
    consumer.accept(supplier.get());
}

onde vamos para o outro lado: definimos o tipo de Suppliere restringimos que ele pode ser colocado no Consumer.

Nós podemos até fazer

public <A extends Animal> void passOn(Consumer<? super A> consumer, Supplier<? extends A> supplier) {
    consumer.accept(supplier.get());
}

onde, tendo as relações intuitivas Life-> Animal-> Mammal-> Dog, Catetc., poderíamos até mesmo colocar um Mammalem um Lifeconsumidor, mas não Stringem um Lifeconsumidor.

glglgl
fonte
1
Entre as 4 versões, o número 2 provavelmente está incorreto. por exemplo, não podemos chamá-lo com (Consumer<Runnable>, Supplier<Dog>)while Dogé subtipo deAnimal & Runnable
ZhongYu 27/07/2015
5

A lógica básica para esse comportamento é Genericsseguir um mecanismo do tipo apagamento. Portanto, em tempo de execução, você não tem como identificar o tipo collectiondiferente de arraysonde não há esse processo de apagamento. Então, voltando à sua pergunta ...

Então, suponha que exista um método conforme indicado abaixo:

add(List<Animal>){
    //You can add List<Dog or List<Cat> and this will compile as per rules of polymorphism
}

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

Hitesh
fonte
4

A subtipagem é invariável para tipos parametrizados. Por mais difícil que a classe Dogseja um subtipo de Animal, o tipo parametrizado List<Dog>não é um subtipo de List<Animal>. Por outro lado, a subtipo covariante é usada por matrizes, portanto, o tipo de matriz Dog[]é um subtipo de Animal[].

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:

List<Dog> dogs = new ArrayList<Dog>(1);
List<Animal> animals = dogs;
animals.add(new Cat()); // compile-time error
Dog dog = dogs.get(0);

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.

Dog[] dogs = new Dog[1];
Object[] animals = dogs;
animals[0] = new Cat(); // run-time error
Dog dog = dogs[0];

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 javapclasse abaixo:

import java.util.ArrayList;
import java.util.List;

public class Demonstration {
    public void normal() {
        List normal = new ArrayList(1);
        normal.add("lorem ipsum");
    }

    public void parameterized() {
        List<String> parameterized = new ArrayList<>(1);
        parameterized.add("lorem ipsum");
    }
}

Usando o comando javap -c Demonstration, isso mostra o seguinte bytecode Java:

Compiled from "Demonstration.java"
public class Demonstration {
  public Demonstration();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public void normal();
    Code:
       0: new           #2                  // class java/util/ArrayList
       3: dup
       4: iconst_1
       5: invokespecial #3                  // Method java/util/ArrayList."<init>":(I)V
       8: astore_1
       9: aload_1
      10: ldc           #4                  // String lorem ipsum
      12: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
      17: pop
      18: return

  public void parameterized();
    Code:
       0: new           #2                  // class java/util/ArrayList
       3: dup
       4: iconst_1
       5: invokespecial #3                  // Method java/util/ArrayList."<init>":(I)V
       8: astore_1
       9: aload_1
      10: ldc           #4                  // String lorem ipsum
      12: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
      17: pop
      18: return
}

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.

Raiz G
fonte
3

Na verdade, você pode usar uma interface para alcançar o que deseja.

public interface Animal {
    String getName();
    String getVoice();
}
public class Dog implements Animal{
    @Override 
    String getName(){return "Dog";}
    @Override
    String getVoice(){return "woof!";}

}

você pode usar as coleções usando

List <Animal> animalGroup = new ArrayList<Animal>();
animalGroup.add(new Dog());
Angel Koh
fonte
1

Se você tem certeza de que os itens da lista são subclasses desse super tipo, você pode converter a lista usando esta abordagem:

(List<Animal>) (List<?>) dogs

Isso é útil quando você deseja passar a lista em um construtor ou iterar sobre ela

sagits
fonte
2
Isto irá criar mais problemas do que realmente resolve
Ferrybig
Se você tentar adicionar um gato à lista, com certeza ele criará problemas, mas para fins de loop eu acho que é a única resposta não detalhada.
sagits
1

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>-lo Collection<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 a Collection<? extends T>a Collection<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 usar Collection<? extends Object>neste caso, mas estou simplificando a ilustração usando DownCastCollection.

/**Could use Collection<? extends Object> and that is the better choice. 
* But I am doing this to illustrate how to use DownCastCollection. **/

public static void print(Collection<Object> col){  
    for(Object obj : col){
    System.out.println(obj);
    }
}
public static void main(String[] args){
  ArrayList<String> list = new ArrayList<>();
  list.addAll(Arrays.asList("a","b","c"));
  print(new DownCastCollection<Object>(list));
}

Agora a turma:

import java.util.AbstractCollection;
import java.util.Collection;
import java.util.Iterator;
import java.util.NoSuchElementException;

public class DownCastCollection<E> extends AbstractCollection<E> implements Collection<E> {
private Collection<? extends E> delegate;

public DownCastCollection(Collection<? extends E> delegate) {
    super();
    this.delegate = delegate;
}

@Override
public int size() {
    return delegate ==null ? 0 : delegate.size();
}

@Override
public boolean isEmpty() {
    return delegate==null || delegate.isEmpty();
}

@Override
public boolean contains(Object o) {
    if(isEmpty()) return false;
    return delegate.contains(o);
}
private class MyIterator implements Iterator<E>{
    Iterator<? extends E> delegateIterator;

    protected MyIterator() {
        super();
        this.delegateIterator = delegate == null ? null :delegate.iterator();
    }

    @Override
    public boolean hasNext() {
        return delegateIterator != null && delegateIterator.hasNext();
    }

    @Override
    public  E next() {
        if(!hasNext()) throw new NoSuchElementException("The iterator is empty");
        return delegateIterator.next();
    }

    @Override
    public void remove() {
        delegateIterator.remove();

    }

}
@Override
public Iterator<E> iterator() {
    return new MyIterator();
}



@Override
public boolean add(E e) {
    throw new UnsupportedOperationException();
}

@Override
public boolean remove(Object o) {
    if(delegate == null) return false;
    return delegate.remove(o);
}

@Override
public boolean containsAll(Collection<?> c) {
    if(delegate==null) return false;
    return delegate.containsAll(c);
}

@Override
public boolean addAll(Collection<? extends E> c) {
    throw new UnsupportedOperationException();
}

@Override
public boolean removeAll(Collection<?> c) {
    if(delegate == null) return false;
    return delegate.removeAll(c);
}

@Override
public boolean retainAll(Collection<?> c) {
    if(delegate == null) return false;
    return delegate.retainAll(c);
}

@Override
public void clear() {
    if(delegate == null) return;
        delegate.clear();

}

}

dan b
fonte
Essa é uma boa idéia, tanto que já existe no Java SE. ; )Collections.unmodifiableCollection
Radiodef
1
Certo, mas a coleção que eu defino pode ser modificada.
dan b
Sim, pode ser modificado. 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 a addoperação, ela lança uma exceção, mesmo que você a tenha convertido.
Vlasec
0

Vamos pegar o exemplo do tutorial do JavaSE

public abstract class Shape {
    public abstract void draw(Canvas c);
}

public class Circle extends Shape {
    private int x, y, radius;
    public void draw(Canvas c) {
        ...
    }
}

public class Rectangle extends Shape {
    private int x, y, width, height;
    public void draw(Canvas c) {
        ...
    }
}

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:

// drawAll method call
drawAll(circleList);


public void drawAll(List<Shape> shapes) {
   shapes.add(new Rectangle());    
}

Então, os "arquitetos" Java tinham 2 opções que abordam esse problema:

  1. não considere que um subtipo é implicitamente supertipo e dê um erro de compilação, como acontece agora

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

aurelius
fonte
0

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 de Listnã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 Listinstâncias. A especialização de um Listpreenchimento do argumento genérico não estende a classe, apenas faz funcionar para esse argumento genérico específico.

Cristik
fonte
0

O problema foi bem identificado. Mas há uma solução; tornar doSomething genérico:

<T extends Animal> void doSomething<List<T> animals) {
}

agora você pode chamar doSomething com List <Dog> ou List <Cat> ou List <Animal>.

gerardw
fonte
0

outra solução é criar uma nova lista

List<Dog> dogs = new ArrayList<Dog>(); 
List<Animal> animals = new ArrayList<Animal>(dogs);
animals.add(new Cat());
ejaenv
fonte
0

Além da resposta de Jon Skeet, que usa este código de exemplo:

// Illegal code - because otherwise life would be Bad
List<Dog> dogs = new ArrayList<Dog>(); // ArrayList implements List
List<Animal> animals = dogs; // Awooga awooga
animals.add(new Cat());
Dog dog = dogs.get(0); // This should be safe, right?

No nível mais profundo, o problema aqui é esse dogse animalscompartilhe 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:

// This code is fine
List<Dog> dogs = new ArrayList<Dog>();
dogs.add(new Dog());
List<Animal> animals = new ArrayList<>(dogs); // Copy list
animals.add(new Cat());
Dog dog = dogs.get(0);   // This is fine now, because it does not return the Cat

Após ligar List<Animal> animals = new ArrayList<>(dogs);, você não pode atribuir diretamente diretamente animalsa um dogsou a cats:

// These are both illegal
dogs = animals;
cats = animals;

portanto, você não pode colocar o subtipo errado de Animalna lista, porque não há subtipo errado - qualquer objeto do subtipo ? extends Animalpode ser adicionado animals.

Obviamente, isso muda a semântica, uma vez que as listas animalse dogsdeixam 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 um Catpode ser adicionado a uma lista que só é deveria conter Dogobjetos). 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.

Luke Hutchison
fonte