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 Animal
ou 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 Animal
e 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).
addAnimals(List<Animal>)
e adicionar uma lista de gatos!