¿List es una subclase de List? ¿Por qué los genéricos de Java no son implícitamente polimórficos?


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

Asume la siguiente jerarquía -

Animal (Padre)

Perro - Cat (Niños)

Así que supongamos que tengo un método doSomething(List<Animal> animals). Por todas las reglas de herencia y polimorfismo, yo asumiría que un List<Dog> es a List<Animal> y a List<Cat> es a List<Animal> - por lo que cualquiera de los dos podría pasarse a este método. No es así. Si quiero lograr esto comportamiento, tengo que decirle explícitamente al método que acepte una lista de cualquier subconjunto 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 generalmente está implícito, pero cuando se trata de genéricos debe especificarse?

Author: kevinarpe, 2010-04-30

16 answers

No, a List<Dog> es no a List<Animal>. Considera lo que puedes hacer con un List<Animal> - puedes agregarle cualquier animal... incluyendo un gato. Ahora, ¿puedes lógicamente añadir un gato a una camada de cachorros? Absolutamente no.

// 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 puedes agregar un Cat a un List<? extends Animal> porque no sabes que es un List<Cat>. Puede recuperar un valor y saber que será un Animal, pero no puede agregar animales arbitrarios. El inverso es cierto para List<? super Animal> - en ese caso puede agregar un Animal a él de forma segura, pero no sabe nada acerca de lo que podría recuperarse de él, porque podría ser un List<Object>.

 776
Author: Jon Skeet,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-08-25 06:57:14

Lo que estás buscando se llama tipo de covariante parámetros . El problema es que no son seguras para el tipo en el caso general, específicamente para listas mutables. Supongamos que tiene un List<Dog>, y se le permite funcionar como un List<Animal>. ¿Qué sucede cuando intentas agregar un Gato a este List<Animal> que es realmente un List<Dog>? Por lo tanto, permitir automáticamente que los parámetros de tipo sean covariantes rompe el sistema de tipos.

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

 69
Author: Michael Ekstrand,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-02-10 08:33:57

Una List<Dog> no a List<Animal>, es que, por ejemplo, puede insertar un Cat en a 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 desde un List<Dog> es similar a leer desde un List<Animal> but pero no escribir.

Los Genéricos en el Lenguaje Java y la Sección sobre Genéricos de los Tutoriales de Java tienen una muy buena explicación en profundidad de por qué algunas cosas son o no polimórficas o permitido con genéricos.

 42
Author: Michael Aaron Safyan,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-04-18 19:21:29

Yo diría que todo el punto de los genéricos es que no permite eso. Considere la situación con los arrays, que permiten ese tipo de covarianza:

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

Ese código compila bien, pero lanza un error de tiempo de ejecución (java.lang.ArrayStoreException: java.lang.Boolean en la segunda línea). No es typesafe. El punto de los Genéricos es agregar la seguridad del tipo de tiempo de compilación, de lo contrario podría quedarse con una clase simple sin genéricos.

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

 32
Author: Yishai,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-08-15 17:33:49

Un punto que creo que debe añadirse a lo que otros respuestas mención es que mientras

List<Dog> isn't-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 es no es Java 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".

Más generalmente, La intuición de OP se presta hacia un lenguaje en el que las operaciones sobre objetos pueden cambiar su tipo, o más bien, el tipo(s) de un objeto es una función (dinámica) de su valor.

 28
Author: einpoklum,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-09-29 14:47:54

Para entender el problema es útil hacer una comparación con los arrays.

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

Los arrays son reificables y covariantes.
Reifiable significa que su información de tipo está completamente disponible en tiempo de ejecución.
Por lo tanto, los arrays proporcionan seguridad de tipo en tiempo de ejecución, pero no seguridad de tipo en 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 viceversa para los genéricos:
Medicamentos Genéricos son borrados e invariantes.
Por lo tanto, los genéricos no pueden proporcionar seguridad de tipo en tiempo de ejecución, pero proporcionan seguridad de tipo en tiempo de compilación.
En el siguiente código si los genéricos eran covariantes, será posible hacer contaminación en 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());
 5
Author: outdev,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-09-29 20:01:43

Las respuestas dadas aquí no me convencieron completamente. Así que en su lugar, 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 para Animal s. Si tienes un Mammal consumidor, pero un Duck proveedor, no deben caber aunque ambos sean animales. Para no permitir esto, se han añadido restricciones adicionales.

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

Por ejemplo,

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

Hace seguro que solo podemos utilizar un proveedor que nos proporciona 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());
}

Donde vamos por el otro camino: definimos el tipo de la Supplier y restringimos que se puede poner en la 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 intuitivasLife -> Animal -> Mammal -> Dog, Cat etc., incluso podríamos poner un Mammal en un consumidor Life, pero no un String en un consumidor Life.

 4
Author: glglgl,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-02-14 21:26:06

La lógica de base para tal comportamiento es que Generics sigue un mecanismo de tipo erasure. Así que en tiempo de ejecución no tienes manera si identificar el tipo de collection a diferencia de arrays donde no hay tal proceso de borrado. Volviendo a tu pregunta...

Así que 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 que el llamante agregue una Lista de tipo Animal a este método, entonces puede agregar algo incorrecto a la colección y en tiempo de ejecución también se ejecutará debido al borrado de tipo. Mientras que en caso de matrices obtendrá una excepción de tiempo de ejecución para tales escenarios...

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

 4
Author: Hitesh,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-07-03 08:34:57

En realidad puedes usar una interfaz para lograr lo que quieres.

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

}

Luego puede usar las colecciones usando

List <Animal> animalGroup = new ArrayList<Animal>();
animalGroup.add(new Dog());
 3
Author: Angel Koh,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-07-12 04:14:24

Si está seguro de que los elementos de la lista son subclases de ese súper tipo dado, puede convertir la lista usando este enfoque:

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

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

 1
Author: sagits,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-01-28 21:11:04

La respuesta así como otras respuestas son correctas. Voy a añadir 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 descomponen. Incluso quitar está bien.

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

}

}

 1
Author: dan b,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-11-07 09:13:01

El subtipo es invariante para los tipos parametrizados. Incluso difícil la clase Dog es un subtipo de Animal, el tipo parametrizado List<Dog> no es un subtipo de List<Animal>. Por el contrario, covariante subtipo es utilizado por los arrays, por lo que la matriz tipo Dog[] es un subtipo de Animal[].

El subtipo invariante asegura que no se violen 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);

Como declaró @Jon Skeet, este código es ilegal, porque de lo contrario violaría las restricciones de tipo devolviendo un gato cuando un perro lo esperaba.

Es instructivo comparar lo anterior con 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, lanza una excepción de almacén de matriz . Una matriz lleva su tipo en tiempo de ejecución de esta manera JVM puede hacer cumplir tipo seguridad del subtipo covariante.

Para entender esto más a fondo echemos un vistazo al bytecode generado por javap de la clase abajo:

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 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 el código traducido de los cuerpos de método son idénticos. 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 los tipos parametrizados no son más que azúcar sintáctica.

 1
Author: Root G,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-05-03 14:00:22

Tomemos el ejemplo de JavaSE tutorial

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) es debido a esta situación:

// drawAll method call
drawAll(circleList);


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

Así que Java "arquitectos" tenía 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 el subtipo como su supertipo y restrinja en compile el método" add " (así que en el método drawAll, si se pasa una lista de círculos, subtipo de forma, el compilador debería detectarlo y restringirlo con el error de compilación para hacerlo).

Por razones obvias, que eligió el primer camino.

 0
Author: aurelius,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-02-27 13:00:01

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

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

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

 0
Author: Cristik,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-03-02 07:48:43

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

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

Ahora puede llamar a doSomething con List o List o List.

 0
Author: gerardw,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-03-28 18:56:11

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());
 0
Author: ejaenv,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-07-20 14:12:13