Java: use polimorfismo o parámetros de tipo acotado

17

Supongamos que tengo esta jerarquía de clases ...

public abstract class Animal {
    public abstract void eat();
    public abstract void talk();
}
class Dog extends Animal {
    @Override
    public void eat() {
    }

    @Override
    public void talk() {
    }
}

class Cat extends Animal {
    @Override
    public void eat() {
    }

    @Override
    public void talk() {
    }
}

Y luego tengo ...

public static <T extends Animal> void addAnimal(T animal) {
    animal.eat();
    animal.talk();
}

public static void addAnimalPoly(Animal animal) {
    animal.eat();
    animal.talk();
}

¿Cuál es la diferencia cuando se usan parámetros de tipo acotado o polimorfismo?

¿Y cuándo usar uno u otro?

HugoMelo
fuente
1
Estas dos definiciones no se benefician mucho de los parámetros de tipo. ¡Pero intente escribir addAnimals(List<Animal>)y agregar una Lista de gatos!
Kilian Foth
77
Bueno, por ejemplo, si cada uno de los métodos anteriores devuelve algo también, entonces el que usa genéricos podría devolver T, mientras que el otro solo podría devolver Animal. Entonces, para la persona que usa esos métodos, en el primer caso obtendría exactamente lo que quería: Dod dog = addAnimal (new Dog ()); mientras que con el segundo método, se vería obligado a lanzar para obtener un perro: Perro d = (Perro) addAnimalPoly (nuevo Perro ());
Shivan Dragon
La mayor parte de mi uso de los tipos acotados es un empapelado sobre la pobre implementación de genéricos de Java (List <T> tiene diferentes reglas de variación para T solo, por ejemplo). En este caso no hay ninguna ventaja, que es esencialmente dos formas de expresar el mismo concepto, aunque como afirma @ShivanDragon que no significa que tenga una T en tiempo de compilación en lugar de un animal. Solo puede tratarlo como un animal internamente, pero puede ofrecerlo como una T externamente.
Phoshi

Respuestas:

14

Estos dos ejemplos son equivalentes y, de hecho, se compilarán con el mismo código de bytes.

Hay dos formas en que agregar un tipo genérico acotado a un método como en su primer ejemplo hará cualquier cosa.

Pasar el parámetro de tipo a otro tipo

Estas dos firmas de métodos terminan siendo las mismas en el código de bytes, pero el compilador impone la seguridad de tipo:

public static <T extends Animal> void addAnimals(Collection<T> animals)

public static void addAnimals(Collection<Animal> animals)

En el primer caso, solo se permite un Collection(o subtipo) de Animal. En el segundo caso, se permite un Collection(o subtipo) con un tipo genérico Animalo un subtipo.

Por ejemplo, lo siguiente está permitido en el primer método pero no en el segundo:

List<Cat> cats = new ArrayList<Cat>();
cats.add(new Cat());
addAnimals(cats);

La razón es que el segundo solo permite colecciones de animales, mientras que el primero permite colecciones de cualquier objeto que se pueda asignar a un animal (es decir, subtipos). Tenga en cuenta que si esta lista fuera una lista de animales que por casualidad contienen un gato, cualquiera de los métodos lo aceptaría: el problema es la especificación genérica de la colección, no lo que realmente contiene.

Devolviendo objetos

La otra vez que importa es con la devolución de objetos. Supongamos que existiera el siguiente método:

public static <T extends Animal> T feed(T animal) {
  animal.eat();
  return animal;
}

Podrías hacer lo siguiente con él:

Cat c1 = new Cat();
Cat c2 = feed(c1);

Si bien este es un ejemplo artificial, hay casos en los que tiene sentido. Sin genéricos, el método tendría que regresar Animaly necesitaría agregar conversión de tipos para que funcione (que es lo que el compilador agrega al código de bytes de todos modos detrás de escena).


fuente
" En el primer caso, solo se permite una Colección (o subtipo) de Animal. En el segundo caso, se permite una Colección (o subtipo) con un tipo genérico de Animal o un subtipo " . - ¿Desea duplicar? comprueba tu lógica allí?
Financia la demanda de Mónica el
3

Use genéricos en lugar de abatir. "Downcasting" es malo, pasando de un tipo más general a uno más específico:

Animal a = hunter.captureOne();
Cat c = (Cat)a;  // ACK!!!!!! What if it's a Dog? ClassCastException!

... confías en que aes un gato, pero el compilador no puede garantizarlo. Podría resultar ser un perro en tiempo de ejecución.

Aquí es donde usarías genéricos:

public class <A> Hunter() {
    public A captureOne() { ... }
}

Ahora puede especificar que desea un cazador de gatos:

Hunter<Cat> hunterC = new Hunter<Cat>();
Cat c = hunterC.captureOne();

Hunter<Dog> hunterD = new Hunter<Dog>();
Dog d = hunterD.captureOne();

Ahora el compilador puede garantizar que hunterC solo capturará gatos, y hunterD solo capturará perros.

Entonces, solo use el polimorfismo regular si solo desea manejar clases específicas como su tipo base. La transmisión es algo bueno. Pero si se encuentra en una situación en la que necesita manejar clases específicas como su propio tipo, genéricamente, use genéricos.

O, realmente, si encuentra que tiene que abatir, use genéricos.

EDITAR: el caso más general es cuando desea posponer la decisión de qué tipos de tipos manejar. Entonces los tipos se convierten en un parámetro, así como los valores.

Digamos que quiero que mi clase de zoológico maneje gatos o esponjas. No tengo una superclase común. Pero aún puedo usar:

public class <T> Zoo() { ... }

Zoo<Sponge> spongeZoo = ...
Zoo<Cat> catZoo = ...

el grado en que lo bloquee depende de lo que esté tratando de hacer;)

Robar
fuente
2

Esta pregunta es antigua, pero un factor importante a considerar parece haberse omitido con respecto a cuándo usar el polimorfismo frente a los parámetros de tipo acotado. Este factor puede ser ligeramente tangente al ejemplo dado en la pregunta, pero creo que es muy relevante para el más general "¿Cuándo usar el polimorfismo frente a los parámetros de tipo acotado?"

TL; DR

Si alguna vez se encuentra moviendo el código de una subclase a una clase base en contra de su mejor juicio, debido a la imposibilidad de acceder a él de forma polimórfica, los parámetros de tipo acotado pueden ser una posible solución.

La respuesta completa

Los parámetros de tipo acotado pueden exponer métodos de subclase concretos no heredados para una variable miembro heredada. El polimorfismo no puede

Para elaborar ampliando su ejemplo:

public abstract class AnimalOwner<T extends Animal> {
   protected T pet;
   public abstract void rewardPet();
}

// Modify the dog class
class Dog extends Animal {
   // ...
   // This method is not inherited from anywhere!
   public void scratchBelly() {
      System.out.println("Belly: Scratched");
   }
}

class DogOwner extends AnimalOwner<Dog> {
   DogOwner(Dog dog) {
     this.pet = dog;
   }

   @Override
   public void rewardPet()
   {
      // ---- Note this call ----
      pet.scratchBelly();
   }
}

Si se definió que la clase abstracta AnimalOwner tiene un protected Animal pet;y opta por el polimorfismo, el compilador generará un error en la pet.scratchBelly();línea, diciéndole que este método no está definido para Animal.

prestamista9
fuente
1

En su ejemplo, usted no (y no debe) usar el tipo acotado. Solo use parámetros de tipo acotado cuando sea necesario , ya que son más confusos de entender.

Aquí hay alguna situación en la que usará parámetros de tipo acotado:

  • Parámetros de colecciones

    class Zoo {
    
      private List<Animal> animals;
    
      public void add(Collection<? extends Animal> newAnimals) {
        animals.addAll(newAnimals);
      }
    }

    entonces puedes llamar

    List<Dog> dogs = ...
    zoo.add(dogs);

    zoo.add(dogs)no compilaría sin él <? extends Animal>, porque los genéricos no son covariantes.

  • Subclases

    abstract class Warrior<T extends Weapon> {
    
      public abstract T getWeapon();
    }

    para limitar el tipo que puede proporcionar la subclase.

También puede usar varios límites <T extends A1 & A2 & A3>para asegurarse de que un tipo sea un subtipo de todos los tipos de la lista.


fuente