Java - Use parâmetros de polimorfismo ou de tipo limitado


17

Suponha que eu tenha essa hierarquia de classes ...

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() {
    }
}

E então eu tenho ....

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

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

Qual é a diferença ao usar parâmetros de tipo limitado ou polimorfismo?

E quando usar um ou outro?


1
Essas duas definições não lucram muito com os parâmetros de tipo. Mas tente escrever addAnimals(List<Animal>)e adicionar uma lista de gatos!
Kilian Foth

7
Bem, por exemplo, se cada um dos métodos acima retornasse algo além, então um usando genéricos poderia retornar T, enquanto o outro só poderia retornar Animal. Portanto, para a pessoa que usa esses métodos, no primeiro caso, ele recuperaria exatamente o que queria: Dod dog = addAnimal (new Dog ()); enquanto no segundo método, ele seria forçado a fazer o cast para conseguir um cachorro: Dog d = (Dog) addAnimalPoly (new Dog ());
Shivan Dragon

A maior parte do meu uso de tipos limitados é um papel de parede sobre a má implementação de genéricos do Java (a Lista <T> possui regras de variação diferentes para T sozinho, por exemplo). Nesse caso, não há vantagem, são essencialmente duas maneiras de expressar o mesmo conceito, embora, como o @ShivanDragon afirme, isso significa que você tem um T na compilação em vez de um Animal. Você só pode tratá-lo internamente como um animal, mas pode oferecê-lo como um T. externamente.
Phoshi 06/02

Respostas:


14

Esses dois exemplos são equivalentes e, de fato, serão compilados no mesmo bytecode.

Há duas maneiras pelas quais a adição de um tipo genérico limitado a um método, como no seu primeiro exemplo, fará qualquer coisa.

Passando o parâmetro type para outro tipo

Essas duas assinaturas de método acabam sendo as mesmas no código de bytes, mas o compilador impõe segurança de tipo:

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

public static void addAnimals(Collection<Animal> animals)

No primeiro caso, apenas um Collection(ou subtipo) de Animalé permitido. No segundo caso, é permitido um Collection(ou subtipo) com um tipo genérico Animalou um subtipo.

Por exemplo, o seguinte é permitido no primeiro método, mas não no segundo:

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

O motivo é que o segundo permite apenas coleções de animais, enquanto o primeiro permite coleções de qualquer objeto atribuível ao animal (ou seja, subtipos). Observe que se essa lista fosse uma lista de animais que continham um gato, qualquer um dos métodos a aceitaria: o problema é a especificação genérica da coleção, não o que ela realmente contém.

Retornando objetos

A outra vez que importa é com objetos retornados. Vamos supor que o seguinte método existisse:

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

Você seria capaz de fazer o seguinte com ele:

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

Embora este seja um exemplo artificial, há casos em que faz sentido. Sem os genéricos, o método precisaria retornar Animale você precisaria adicionar conversão de tipo para fazê-lo funcionar (que é o que o compilador adiciona ao código de bytes de qualquer maneira nos bastidores).


" No primeiro caso, somente uma coleção (ou subtipo) de animal é permitida. No segundo caso, uma coleção (ou subtipo) com um tipo genérico de animal ou subtipo é permitida. " - Você deseja duplicar verifique sua lógica aí?
Fund Monica's Lawsuit

3

Use genéricos em vez de fazer downcast. "Downcasting" é ruim, passando de um tipo mais geral para um mais específico:

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

... você confia que aé um gato, mas o compilador não pode garantir isso. Pode ser um cachorro em tempo de execução.

Aqui é onde você usaria genéricos:

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

Agora você pode especificar que deseja um caçador de gatos:

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

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

Agora, o compilador pode gurantee que hunterC só irá capturar os gatos, e apenas os cães de captura hunterD.

Portanto, use polimorfismo regular se quiser lidar com classes específicas como seu tipo base. Upcasting é uma coisa boa. Mas se você entrar em uma situação em que precisa lidar com classes específicas como seu próprio tipo, use genericamente os genéricos.

Ou, realmente, se você achar que precisa fazer um downcast, use genéricos.

EDIT: o caso mais geral é quando você deseja adiar a decisão de quais tipos de tipos manipular. Portanto, os tipos se tornam um parâmetro, assim como os valores.

Digamos que eu queira que minha aula de zoológico lide com gatos ou esponjas. Eu não tenho uma super classe comum. Mas ainda posso usar:

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

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

o grau em que você bloqueia isso depende do que você está tentando fazer;)


2

Essa questão é antiga, mas um fator importante a ser considerado parece ter sido deixado de fora em relação a quando usar parâmetros de polimorfismo versus tipo limitado. Esse fator pode ser um pouco tangente ao exemplo dado na pergunta, mas eu acho muito relevante para os parâmetros mais gerais "Quando usar polimorfismo versus parâmetros do tipo delimitado?"

TL; DR

Se você se mudar do código de uma subclasse para uma classe base contra seu melhor julgamento, devido à incapacidade de acessá-lo de forma polimórfica, os parâmetros do tipo limitado podem ser uma solução potencial.

A resposta completa

Os parâmetros do tipo delimitado podem expor métodos de subclasse concretos e não herdados para uma variável de membro herdada. O polimorfismo não pode

Para elaborar estendendo seu exemplo:

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

Se a classe abstrata AnimalOwner tiver sido definida para ter um protected Animal pet;e optar pelo polimorfismo, o compilador apresentará um erro na pet.scratchBelly();linha, informando que esse método não está definido para Animal.


1

No seu exemplo, você não (e não deve) usar o tipo delimitado. Use apenas parâmetros de tipo limitado quando for necessário , pois eles são mais confusos para entender.

Aqui estão algumas situações em que você usará parâmetros de tipo limitado:

  • Parâmetros de coleções

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

    então você pode ligar

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

    zoo.add(dogs)falharia em compilar sem <? extends Animal>, porque os genéricos não são covariantes.

  • Subclassificação

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

    para limitar o tipo que a subclasse pode fornecer.

Você também pode usar vários limites <T extends A1 & A2 & A3>para garantir que um tipo seja um subtipo de todos os tipos na lista.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.