metodos herencia genericos generica generic ejemplos ejemplo clase arquitectura animal java generics inheritance polymorphism

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



list generica java (16)

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 <Dog> o List <Cat> o List <Animal>.

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

Supongamos la siguiente jerarquía:

Animal (padre)

Perro - Gato (Niños)

Supongamos que tengo un método doSomething(List<Animal> animals) . Por todas las reglas de herencia y polimorfismo, supongo que un List<Dog> List<Animal> es un List<Animal> y un List<Cat> List<Animal> es un List<Animal> , por lo que se puede pasar cualquiera de los dos a este método. No tan. Si quiero lograr este comportamiento, debo 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 es generalmente implícito, pero cuando se trata de genéricos, debe especificarse?


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!";}

}

A continuación, puede utilizar las colecciones utilizando

List <Animal> animalGroup = new ArrayList<Animal>(); animalGroup.add(new Dog());


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

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

}


La lógica básica para tal comportamiento es que los Generics siguen un mecanismo de borrado de tipo. Por lo tanto, en el tiempo de ejecución no tiene forma de identificar el tipo de collection diferencia de las arrays en las que no existe tal proceso de borrado. Así que 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 a la persona que llama agregar la Lista de tipo Animal a este método, entonces puede agregar algo incorrecto en la colección y en el tiempo de ejecución también se ejecutará debido al borrado de tipo. Mientras que en el caso de matrices, obtendrá una excepción de tiempo de ejecución para tales escenarios ...

Por lo tanto, 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 ...


La razón por la que una List<Dog> no es una List<Animal> es que, por ejemplo, puede insertar un Cat en una List<Animal> , pero no en una List<Dog> ... puede usar comodines para hacer genéricos más extensibles cuando sea posible; por ejemplo, leer de una List<Dog> es similar a leer de una 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 muy buena y profunda explicación de por qué algunas cosas son o no polimorfas o están permitidas con los genéricos.


Las respuestas dadas aquí no me convencieron completamente. Así que en cambio, hago otro ejemplo.

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

suena bien, ¿no? Pero solo puede pasar el Consumer y el Supplier para el Animal . Si usted tiene un consumidor de Mammal , pero un proveedor de Duck , 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 utilizamos.

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 utilizar un proveedor que nos proporcione el tipo de objeto correcto 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 lado: definimos el tipo de Supplier y restringimos que se pueda colocar 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 , Cat , etc., incluso podríamos poner un Mammal en un consumidor de Life , pero no una String en un consumidor de Life .


Lo que buscas 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, Animal se puede reemplazar con Dog ), lo mismo se aplica a las expresiones que usan esos objetos (por lo tanto, List<Animal> podría reemplazarse con List<Dog> ). El problema es que la covarianza no es segura para las listas mutables en general. Supongamos que tiene una List<Dog> , y se está utilizando como una List<Animal> . ¿Qué sucede cuando intenta agregar un Gato a esta List<Animal> que es realmente una List<Dog> ? Automáticamente permitiendo que los parámetros de tipo sean covariantes rompe el sistema de tipo.

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


No, una List<Dog> no es una List<Animal> . Considere lo que puede hacer con una List<Animal> : puede agregarle cualquier animal ... incluido un gato. Ahora, ¿puedes agregar lógicamente 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 gato muy confundido.

Ahora, no puedes agregar un Cat a una List<? extends Animal> List<? extends Animal> porque no sabes que es una List<Cat> . Puede recuperar un valor y saber que será un Animal , pero no puede agregar animales arbitrarios. Lo contrario es cierto para la List<? super Animal> List<? super Animal> : en ese caso, puedes agregarle un Animal de forma segura, pero no sabes nada sobre lo que se puede recuperar, porque podría ser una List<Object> .


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


Para entender el problema es útil hacer comparaciones con arreglos.

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

Los arreglos son reifiable y covariantes .
reifiable significa que su información de tipo está completamente 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 erased e invariantes .
Por lo tanto, los genéricos no pueden proporcionar seguridad de tipo de tiempo de ejecución, pero sí proporcionan seguridad de tipo de tiempo de compilación.
En el código a continuación, si los genéricos son covariantes, será posible hacer que la contaminación del montón en la línea 3.

List<Dog> dogs = new ArrayList<>(); List<Animal> animals = dogs; // compile-time error, otherwise heap pollution animals.add(new Cat());


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

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

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


Subtipo es invariant para tipos parametrizados. Incluso si la clase Dog es un subtipo de Animal , el tipo parametrizado List<Dog> no es un subtipo de List<Animal> . En contraste, el subtipo invariant es utilizado por los arreglos, por lo que el tipo de arreglo Dog[] es un subtipo de Animal[] .

Los subtipos invariantes aseguran que las restricciones de tipo impuestas por Java no sean violadas. 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 lo indica @Jon Skeet, este código es ilegal, porque de lo contrario violaría las restricciones de tipo al devolver un gato cuando un perro esperaba.

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, lanza una excepción de tienda de matriz . Una matriz transporta su tipo en tiempo de ejecución de esta manera JVM puede imponer la seguridad de tipos de subtipos covariantes.

Para entender esto, veamos el javap de javap generado por javap de la 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 bytecode 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 del método es idéntico. El compilador reemplazó cada tipo parametrizado por su erasure . Esta propiedad es crucial, lo que significa que no rompió la compatibilidad hacia atrás.

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.


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

Por lo tanto, 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 la List no 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, son instancias de List . La especialización de una List completando el argumento genérico no extiende la clase, solo hace que funcione para ese argumento genérico en particular.


Un punto que creo que debería agregarse a lo que mencionan other answers es que mientras

List<Dog> no es una 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 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 quadrapeds.

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

De manera más general, la intuición de OP se presta a un lenguaje en el que las operaciones en objetos pueden cambiar su tipo , o más bien, el tipo (s) de un objeto es una función (dinámica) de su valor.


Vamos a tomar el ejemplo del tutorial 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) es debido 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 solucionaban este problema:

  1. No consideres que un subtipo es implícitamente un supertipo, y da un error de compilación, como sucede ahora

  2. considera que el subtipo es su supertipo y restringe 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 restringirte con el error de compilación para hacer ese).

Por razones obvias, que eligió la primera manera.


Yo diría que el punto central de los genéricos es que no permite eso. Considere la situación con matrices, 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 de tipos seguros. 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 necesitas ser más flexible y eso es lo que ? super Class ? super Class y ? extends Class ? extends Class son para. La primera es cuando necesita insertarse en una Collection tipos (por ejemplo), y la última es para cuando necesita leer de ella, de una manera segura. Pero la única manera de hacer ambas cosas al mismo tiempo es tener un tipo específico.