¿Es List <Dog> una subclase de List <Animal>? ¿Por qué los genéricos de Java no son polimórficos implícitamente?

770

Estoy un poco confundido acerca de cómo los genéricos de Java manejan la herencia / polimorfismo.

Asuma la siguiente jerarquía:

Animal (padre)

Perro - Gato (Niños)

Supongamos que tengo un método doSomething(List<Animal> animals). Según todas las reglas de herencia y polimorfismo, supondría que a List<Dog> es a List<Animal>y a List<Cat> es a List<Animal>, por lo que cualquiera de ellos podría pasarse a este método. No tan. Si quiero lograr este comportamiento, tengo que decirle explícitamente al método que acepte una lista de cualquier subclase de Animal diciendo doSomething(List<? extends Animal> animals).

Entiendo que este es el comportamiento de Java. Mi pregunta es ¿por qué ? ¿Por qué el polimorfismo es generalmente implícito, pero cuando se trata de genéricos debe especificarse?

froadie
fuente
17
Y una pregunta gramatical totalmente no relacionada que me molesta ahora: ¿mi título debería ser "por qué no son los genéricos de Java" o "por qué no son los genéricos de Java"? ¿Es "genérico" plural debido a s o singular porque es una entidad?
froadie
25
los genéricos, como se hace en Java, son una forma muy pobre de polimorfismo paramétrico. No confíes demasiado en ellos (como solía hacerlo), porque un día enfrentarás sus patéticas limitaciones: ¡el cirujano extiende Handable <Scalpel>, Handable <Sponge> KABOOM! Does no computar [TM]. Ahí está su limitación genérica de Java. Cualquier OOA / OOD se puede traducir bien a Java (y MI se puede hacer muy bien usando interfaces Java), pero los genéricos simplemente no funcionan. Están bien para "colecciones" y programación de procedimientos que dicen (que es lo que la mayoría de los programadores de Java hacen de todos modos, así que ...).
SyntaxT3rr0r
8
La superclase de List <Dog> no es List <Animal> sino List <?> (Es decir, lista de tipo desconocido). Los genéricos borran la información de tipo en código compilado. Esto se hace para que el código que usa genéricos (java 5 y superior) sea compatible con versiones anteriores de java sin genéricos.
rai.skumar
99
@froadie ya que nadie parecía responder ... definitivamente debería ser "por qué no son los genéricos de Java ..." El otro problema es que "genérico" es en realidad un adjetivo, por lo que "genérico" se refiere a un sustantivo plural eliminado modificado por "genérico". Se podría decir "esa función es genérica", pero eso sería más engorroso que decir "esa función es genérica". Sin embargo, es un poco engorroso decir "Java tiene funciones y clases genéricas", en lugar de solo "Java tiene genéricos". Como alguien que escribió su tesis de maestría sobre adjetivos, ¡creo que te has topado con una pregunta muy interesante!
dantiston

Respuestas:

916

No, a noList<Dog> es a . Considere lo que puede hacer con un : puede agregarle cualquier animal ... incluido un gato. Ahora, ¿puedes agregar lógicamente un gato a una camada de cachorros? Absolutamente no.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 tienes un muy gato confundido.

Ahora, no puede agregar a Cata List<? extends Animal>porque no sabe que es a List<Cat>. Puede recuperar un valor y saber que será un Animal, pero no puede agregar animales arbitrarios. Lo contrario es cierto para List<? super Animal>: en ese caso, puede agregarle un mensaje Animalde forma segura, pero no sabe nada sobre lo que podría recuperarse, ya que podría ser un List<Object>.

Jon Skeet
fuente
50
Curiosamente, cada lista de perros es de hecho una lista de animales, tal como nos dice la intuición. El punto es que no todas las listas de animales son una lista de perros, por lo tanto, la mutación de la lista al agregar un gato es el problema.
Ingo
66
@Ingo: No, en realidad no: puedes agregar un gato a una lista de animales, pero no puedes agregar un gato a una lista de perros. Una lista de perros es solo una lista de animales si la considera en un sentido de solo lectura.
Jon Skeet
12
@ JonSkeet - Por supuesto, pero ¿quién exige que hacer una nueva lista de un gato y una lista de perros realmente cambie la lista de perros? Esta es una decisión de implementación arbitraria en Java. Uno que va en contra de la lógica y la intuición.
Ingo
77
@Ingo: No habría usado eso "ciertamente" para empezar. Si tiene una lista que dice en la parte superior "Hoteles a los que queremos ir" y luego alguien le agregó una piscina, ¿le parece válido? No, es una lista de hoteles, que no es una lista de edificios. Y no es como si incluso dijera "Una lista de perros no es una lista de animales". Lo pongo en términos de código , en una fuente de código. Realmente no creo que haya ninguna ambigüedad aquí. Usar la subclase sería incorrecto de todos modos: se trata de compatibilidad de asignación, no de subclase.
Jon Skeet
14
@ruakh: El problema es que luego estás llegando al tiempo de ejecución, algo que puede bloquearse en tiempo de compilación. Y yo diría que la covarianza de la matriz fue un error de diseño para empezar.
Jon Skeet
84

Lo que está buscando se llama parámetros de tipo covariante . Esto significa que si un tipo de objeto se puede sustituir por otro en un método (por ejemplo, Animalse puede reemplazar con Dog), lo mismo se aplica a las expresiones que usan esos objetos (por lo que List<Animal>podría reemplazarse por List<Dog>). El problema es que la covarianza no es segura para listas mutables en general. Supongamos que tiene un List<Dog>, y se está utilizando como a List<Animal>. ¿Qué sucede cuando intentas agregar un gato a esto, List<Animal>que es realmente un List<Dog>? Permitir automáticamente que los parámetros de tipo sean covariantes rompe el sistema de tipos.

Sería útil agregar una sintaxis para permitir que los parámetros de tipo se especifiquen como covariantes, lo que evita las ? extends Foodeclaraciones en el método, pero eso agrega complejidad adicional.

Michael Ekstrand
fuente
44

La razón por la cual a List<Dog>no es a List<Animal>, es que, por ejemplo, puede insertar un Caten un List<Animal>, pero no en un List<Dog>... puede usar comodines para hacer que los genéricos sean más extensibles cuando sea posible; por ejemplo, leer de a List<Dog>es similar a leer de a List<Animal>, pero no escribir.

Los genéricos en el lenguaje Java y la sección sobre genéricos de los tutoriales de Java tienen una explicación muy buena y profunda de por qué algunas cosas son o no polimórficas o están permitidas con genéricos.

Michael Aaron Safyan
fuente
36

Un punto que creo que debería agregarse a lo que otras respuestas mencionan es que

List<Dog>no es-a List<Animal> en Java

también es cierto que

Una lista de perros es una lista de animales en inglés (bueno, bajo una interpretación razonable)

La forma en que funciona la intuición del OP, que es completamente válida, por supuesto, es la última oración. Sin embargo, si aplicamos esta intuición obtenemos un lenguaje que no es Java-esque en su sistema de tipos: supongamos que nuestro lenguaje permite agregar un gato a nuestra lista de perros. ¿Qué significaría eso? Significaría que la lista deja de ser una lista de perros, y sigue siendo simplemente una lista de animales. Y una lista de mamíferos, y una lista de cuadrúpedos.

Para decirlo de otra manera: A List<Dog> en Java no significa "una lista de perros" en inglés, significa "una lista que puede tener perros, y nada más".

En términos más generales, la intuición de OP se presta hacia un lenguaje en el que las operaciones en los objetos pueden cambiar su tipo , o más bien, el tipo (s) de un objeto es una función (dinámica) de su valor.

einpoklum
fuente
Sí, el lenguaje humano es más difuso. Pero aún así, una vez que agrega un animal diferente a la lista de perros, sigue siendo una lista de animales, pero ya no es una lista de perros. La diferencia es que, un humano, con la lógica difusa, generalmente no tiene problemas para darse cuenta de eso.
Vlasec
Como alguien que encuentra las constantes comparaciones con las matrices aún más confusas, esta respuesta lo logró para mí. Mi problema fue la intuición del lenguaje.
FLonLon
32

Yo diría que el punto de Generics es que no permite eso. Considere la situación con las matrices, que permiten ese tipo de covarianza:

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

Ese código se compila bien, pero arroja un error de tiempo de ejecución ( java.lang.ArrayStoreException: java.lang.Booleanen la segunda línea). No es de tipo seguro. El objetivo de Generics es agregar la seguridad del tipo de tiempo de compilación, de lo contrario, podría seguir con una clase simple sin genéricos.

Ahora bien, hay momentos en los que necesita para ser más flexible y que es lo que el ? super Classy ? extends Classson para. El primero es cuando necesita insertar en un tipo Collection(por ejemplo), y el segundo es para cuando necesita leer de él, de manera segura. Pero la única forma de hacer ambas cosas al mismo tiempo es tener un tipo específico.

Yishai
fuente
13
Podría decirse que la covarianza de matriz es un error de diseño del lenguaje. Tenga en cuenta que debido a la eliminación de tipo, el mismo comportamiento es técnicamente imposible para la recolección genérica.
Michael Borgwardt
" Diría que el punto de Generics es que no permite eso ". Nunca puede estar seguro: los sistemas de tipos de Java y Scala no son claros: la crisis existencial de punteros nulos (presentado en OOPSLA 2016) (ya que parece corregido)
David Tonhofer
15

Para comprender el problema, es útil hacer una comparación con las matrices.

List<Dog>es no subclase de List<Animal>.
Pero Dog[] es una subclase de Animal[].

Las matrices son reificables y covariantes .
Reificable significa que su información de tipo está totalmente disponible en tiempo de ejecución.
Por lo tanto, las matrices proporcionan seguridad de tipo de tiempo de ejecución pero no seguridad de tipo de tiempo de compilación.

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

Es al revés para los genéricos: los
genéricos son borrados e invariables .
Por lo tanto, los genéricos no pueden proporcionar seguridad de tipo de tiempo de ejecución, pero proporcionan seguridad de tipo de tiempo de compilación.
En el siguiente código, si los genéricos son covariantes, será posible generar contaminación por pilas en la línea 3.

    List<Dog> dogs = new ArrayList<>();
    List<Animal> animals = dogs; // compile-time error, otherwise heap pollution
    animals.add(new Cat());
outdev
fuente
2
Podría argumentarse que, precisamente por eso, las matrices en Java están rotas ,
leonbloy
Las matrices son covariantes es una "característica" del compilador.
Cristik
6

Las respuestas dadas aquí no me convencieron por completo. Entonces, en cambio, hago otro ejemplo.

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

suena bien, ¿no? Pero solo puedes pasar Consumers y Suppliers por Animals. Si tiene un Mammalconsumidor, pero un Duckproveedor, no deben encajar, aunque ambos son animales. Para no permitir esto, se han agregado restricciones adicionales.

En lugar de lo anterior, tenemos que definir relaciones entre los tipos que usamos.

P.ej.,

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

se asegura de que solo podamos usar un proveedor que nos proporcione el tipo de objeto adecuado para el consumidor.

OTOH, también podríamos hacer

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

a dónde vamos en la otra dirección: definimos el tipo de Suppliery restringimos que se pueda poner en el Consumer.

Incluso podemos hacer

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

donde, teniendo las relaciones intuitivas Life-> Animal-> Mammal-> Dog, Catetc., incluso podríamos poner Mammala un Lifeconsumidor, pero no Stringa un Lifeconsumidor.

glglgl
fuente
1
Entre las 4 versiones, el # 2 es probablemente incorrecto. por ejemplo, no podemos llamarlo (Consumer<Runnable>, Supplier<Dog>)mientras Doges subtipo deAnimal & Runnable
ZhongYu
5

La lógica básica para tal comportamiento es que Genericssigue un mecanismo de borrado de tipo. Por lo tanto, en el tiempo de ejecución no tiene forma si identifica el tipo de collectiondiferencia de arraysdonde no existe dicho proceso de borrado. Volviendo a tu pregunta ...

Supongamos que hay un método como se indica a continuación:

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

Ahora, si Java permite a la persona que llama agregar Lista de tipo Animal a este método, entonces puede agregar algo incorrecto a la colección y en el tiempo de ejecución también se ejecutará debido a la eliminación de tipo. Mientras que en el caso de las matrices, obtendrá una excepción de tiempo de ejecución para tales escenarios ...

Así, en esencia, este comportamiento se implementa para que no se pueda agregar algo incorrecto a la colección. Ahora creo que existe la eliminación de tipos para dar compatibilidad con Java heredado sin genéricos ...

Hitesh
fuente
4

El subtipo es invariante para los tipos parametrizados. Incluso si la clase Doges un subtipo de Animal, el tipo parametrizado List<Dog>no es un subtipo de List<Animal>. Por el contrario, las matrices utilizan el subtipo covariante , por lo que el tipo de matriz Dog[]es un subtipo de Animal[].

El subtipo invariable garantiza que no se infrinjan las restricciones de tipo impuestas por Java. Considere el siguiente código dado 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);

Según lo indicado por @ Jon Skeet, este código es ilegal, porque de lo contrario violaría las restricciones de tipo al devolver un gato cuando lo esperaba un perro.

Es instructivo comparar lo anterior con un código análogo para matrices.

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

El código es legal. Sin embargo, arroja una excepción de tienda de matriz . Una matriz lleva su tipo en tiempo de ejecución de esta manera JVM puede hacer cumplir la seguridad de tipo de subtipo covariante.

Para entender esto aún más, veamos el bytecode generado por javapla clase a continuación:

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 el comando javap -c Demonstration, esto muestra el siguiente código de bytes de 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 el código traducido de los cuerpos de los métodos es idéntico. El compilador reemplazó cada tipo parametrizado por su borrado . Esta propiedad es crucial, lo que significa que no rompió la compatibilidad con versiones anteriores.

En conclusión, la seguridad en tiempo de ejecución no es posible para los tipos parametrizados, ya que el compilador reemplaza cada tipo parametrizado por su borrado. Esto hace que los tipos parametrizados no sean más que azúcar sintáctica.

Raíz G
fuente
3

En realidad, puede usar una interfaz para lograr lo que desea.

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

}

entonces puedes usar las colecciones usando

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

Si está seguro de que los elementos de la lista son subclases de ese supertipo dado, puede emitir la lista con este enfoque:

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

Esto es útil cuando desea pasar la lista en un constructor o iterar sobre ella

sagits
fuente
2
Esto creará más problemas de los que realmente resuelve
Ferrybig
Si intentas agregar un gato a la lista, seguro que creará problemas, pero para propósitos de bucle, creo que es la única respuesta no detallada.
cede
1

La respuesta , así como otras respuestas, son correctas. Voy a agregar a esas respuestas una solución que creo que será útil. Creo que esto aparece a menudo en la programación. Una cosa a tener en cuenta es que para las Colecciones (Listas, Conjuntos, etc.) el problema principal es agregar a la Colección. Ahí es donde las cosas se rompen. Incluso eliminar está bien.

En la mayoría de los casos, podemos usar en Collection<? extends T>lugar de eso Collection<T>y esa debería ser la primera opción. Sin embargo, estoy encontrando casos en los que no es fácil hacer eso. Está en discusión si eso es siempre lo mejor que se puede hacer. Estoy presentando aquí una clase DownCastCollection que puede convertir a Collection<? extends T>a a Collection<T>(podemos definir clases similares para List, Set, NavigableSet, ...) para usar cuando usar el enfoque estándar es muy inconveniente. A continuación se muestra un ejemplo de cómo usarlo (también podríamos usarlo Collection<? extends Object>en este caso, pero lo mantengo simple para ilustrar 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));
}

Ahora la clase:

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
fuente
Esta es una buena idea, tanto que ya existe en Java SE. ; )Collections.unmodifiableCollection
Radiodef
1
Correcto, pero la colección que defino se puede modificar.
dan b
Sí, se puede modificar. Collection<? extends E>Sin embargo, ya maneja ese comportamiento correctamente, a menos que lo use de una manera que no sea de tipo seguro (por ejemplo, convertirlo en otra cosa). La única ventaja que veo es que, cuando llamas a la addoperación, arroja una excepción incluso si la lanzaste.
Vlasec
0

Tomemos el ejemplo del tutorial de 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) {
        ...
    }
}

Entonces, ¿por qué una lista de perros (círculos) no debe considerarse implícitamente? Una lista de animales (formas) se debe a esta situación:

// drawAll method call
drawAll(circleList);


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

Así que los "arquitectos" de Java tenían 2 opciones que abordan este problema:

  1. no considere que un subtipo es implícitamente su supertipo, y dé un error de compilación, como sucede ahora

  2. considere que el subtipo es su supertipo y restrinja al compilar el método "agregar" (por lo tanto, en el método drawAll, si se pasara una lista de círculos, subtipo de forma, el compilador debería detectar eso y restringirlo con un error de compilación para hacerlo ese).

Por razones obvias, eso eligió la primera forma.

aurelius
fuente
0

También debemos tener en cuenta cómo el compilador amenaza las clases genéricas: en "instancia" un tipo diferente cada vez que rellenamos los argumentos genéricos.

Así tenemos ListOfAnimal, ListOfDog, ListOfCat, etc, que son clases distintas que terminan siendo "creada" por el compilador cuando especificamos los argumentos genéricos. Y esta es una jerarquía plana (en realidad respecto a Listno es una jerarquía en absoluto).

Otro argumento por el que la covarianza no tiene sentido en el caso de las clases genéricas es el hecho de que, en la base, todas las clases son iguales. List instancias. Especializarse Listen rellenando el argumento genérico no extiende la clase, solo hace que funcione para ese argumento genérico en particular.

Cristik
fuente
0

El problema ha sido bien identificado. Pero hay una solución; hacer doSomething genérica:

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

ahora puede llamar a doSomething con List <Dog> o List <Cat> o List <Animal>.

gerardw
fuente
0

otra solución es construir una nueva lista

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

Además de la respuesta de Jon Skeet, que usa este código de ejemplo:

// 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?

En el nivel más profundo, el problema aquí es eso dogsy animalscompartir una referencia. Eso significa que una forma de hacer que esto funcione sería copiar la lista completa, lo que rompería la igualdad de referencia:

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

Después de llamar List<Animal> animals = new ArrayList<>(dogs);, no se puede asignar posteriormente directamente animalsa cualquiera dogso cats:

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

por lo tanto, no puede colocar el subtipo incorrecto Animalen la lista, porque no hay un subtipo incorrecto; ? extends Animalse puede agregar cualquier objeto de subtipo animals.

Obviamente, esto cambia la semántica, ya que las listas animalsya dogsno se comparten, por lo que agregar a una lista no agrega a la otra (que es exactamente lo que desea, para evitar el problema de que se Catpueda agregar a una lista que solo es se supone que contiene Dogobjetos). Además, copiar toda la lista puede ser ineficiente. Sin embargo, esto resuelve el problema de equivalencia de tipo, al romper la igualdad de referencia.

Luke Hutchison
fuente