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




8 Answers

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

metodos pdf

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?




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.




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.




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 .




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

}

}




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.




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




Related