A motivação de estudar Generics em Java é de poupar o desenvolvedor de códigos redundantes, como é o caso de casting excessivo. Este foi introduzido desde o Java SE 5.0. Vamos neste artigo abordar os principais usos e especificidades de Generics, para que você leitor possa entender o funcionamento do mesmo e utilizá-lo em seus projetos com maior frequência e facilidade.
Para iniciarmos vamos a um exemplo muito comum, apresentado na listagem 1, que mostra como ficaria uma Lista de Objetos com Generics e outra sem Generics.
/* COM GENERICS */
List<Apple> box = ...;
Apple apple = box.get(0);
/* SEM GENERICS */
List box = ...;
/*
Se o objeto retornado de box.get(0) não puder
ser convertido para Apple, só saberemos disso em tempo
de execução
*/
Apple apple = (Apple) box.get(0);
De inicio já podemos notar 2 problemas básicos que são encontrados quando optamos por não utilizar Generics:
- Teremos que fazer um cast para o objeto do tipo Apple toda vez que capturarmos algo da List box;
- Caso algum erro de cast ocorra, só veremos em tempo de execução, pois este cast só será feito assim que este determinado trecho do código for executado. Diferente do Generics, que o erro é em tempo de compilação, ou seja, já nos deparamos com o erro antes mesmo de tentar executar o projeto, o próprio compilador nos avisará que não é possível atribuir um objeto box ao Apple pois estes são de tipos diferentes, veja um exemplo abaixo que já apresenta erro em tempo de compilação.
List<Orange> box = ...;
/*
Erro em tempo de compilação pois
uma lista de Orange não pode ser atribuido a um
objeto do tipo Apple. Isso porque ao fazer “box.get(0)”
estamos retornando um Orange e não um Apple.
*/
Apple apple = box.get(0);
Generic em Classes e Interfaces
Podemos também utilizar os Generics em Classes ou Interfaces. Estes servem como parâmetro para nossa Classe, assim poderemos utilizar esta “variável” em todo escopo de nossa classe. Veja o exemplo de utilização da listagem 3.
public interface List<T> extends Collection<T> {
...
}
Esta é a interface da List em Java, perceba que podemos fazer: List pois a interface nos permite isso. A vantagem de fazer isso é o retorno do Objeto quando fazemos um “get”, veja na listagem 4.
T get(int index);
O método acima irá retornar um objeto do tipo “T” dado determinado index, e quem é T ? Em princípio não sabemos, só vamos descobrir ao implementar a interface. Na listagem 1 o nosso T = Apple.
Sendo assim, o mesmo poderá ser utilizado durante todo desenvolvimento da interface para evitar o uso de castings excessivos. São inúmeras as possibilidades que temos ao se trabalhar com Generics em Classes e Interfaces, muitos problemas que antes seriam resolvidos com horas e até dias de código “sujo”, podem ser resolvidos em apenas algumas linhas, basta ter a habilidade necessária para utilizar tal ferramenta.
Generics em Métodos e Construtores
Antes de começar a explicação, vamos mostrar o exemplo do uso de Generics em um método Java.
public static <T> getFirst(List<T> list)
Perceba na listagem 5 que o método getFirst aceita uma lista do tipo T e retorna um objeto do tipo T.
A iteração em Java (Iterator) também possui Generic, assim um Iterator pode facilmente ser retornado para um novo objeto sem a necessidade do Cast explícito. Veja o exemplo na listagem 6.
for (Iterator<String> iter = str.iterator(); iter.hasNext();) {
String s = iter.next();
System.out.print(s);
O que você percebe acima é o uso do Generic para transformar o Iterator em iterações apenas de String, assim sempre que fizermos “iter.next()” estamos automaticamente retornando uma String sem precisar fazer “String.valueOf(iter.next())”.
O código da listagem 6 também pode ser convertido para um foreach, assim aproveitamos também o recurso de generic que nós é oferecido.
for (String s: str) {
System.out.print(s);
}
Subtipos Genéricos
Antes de iniciar a explicação do funcionamento de subtipos em Generics, é importante entender o conceito em Java. Entenda que em Java a seguinte hierarquia é totalmente possível:
Significa que um Apple é um Fruit e um Fruit é um Object, então um Apple é um Object também. Analogamente, se A é filho B e B é filho de C, então A é filho de C. A referencia contida na listagem 8 só é possível por esta hierarquia imposta no Java.
Apple a = ...;
Fruit f = a;
Tudo isso foi explicado para que possamos finalmente chegar neste assunto incluso no Generics, veja o código abaixo:
List<Apple> apples = ...;
List<Fruit> fruits = apples;
Se seguirmos o raciocínio da explicação acima, o código acima será compilado normalmente sem nenhum erro. Porém isso não é possível no Generics, pois em algumas situações poderia ocorrer uma quebra a consistência da linguagem. Como assim ? Vamos explicar.
Imagine que você tem o código da listagem 9 funcionando normalmente (o que não é o caso). Temos então uma caixa de maças, e transformamos nossa caixa de maças em uma caixa de frutas, pois toda maça é fruta, certo ?
Agora já que temos uma caixa de frutas (que sabemos na verdade que ela é uma caixa de maças), o que nos impede de colocar morangos ? Nada. Veja na listagem 10 o que poderíamos fazer se a listagem 9 compilasse sem erros.
List<Apple> apples = ...;
List<Fruit> fruits = apples;
fruits.add(new Strawberry());
O código acima está logicamente errado, pois antes havíamos atributo a caixa de maças a caixa de frutas, ou seja, nossa atual caixa de frutas na verdade é uma caixa de maças, mas desta forma nada nos impede de colocar um moranga em uma caixa de frutas, isso porque, um morango também é uma fruta. É por este motivo que o Generic não permite esse tipo de atribuição. Podemos resumir isso tudo a apenas uma só frase: Generics são invariantes.
Wildcards
Antes de começar a entender o funcionamento de WildCards em Generics, entenda o porque utilizá-lo. Lembre-se da listagem 10 onde mostramos que não é possível atribuir uma lista de “Apples” a uma lista de “Fruits” mesmo o tipo Apple sendo filho de Fruit, isso porque estaríamos quebrando o “contrato” que diz que Apple só aceita Apple, pois se colocarmos este como Fruit, poderíamos aceitar Morangos, laranjas e etc.
Enfim, com Wildcards solucionamos este problema. Veja como:
Existem 3 tipos de Wildcards em Generics:
- Unknown Wildcard, ou seja, Wildcard desconhecido.
- Extends Wildcard
- Super wildcard
Vamos explicar os 3 nos próximos tópicos.
Unknown Wildcard
Como você não sabe o tipo do objeto, você deve tratá-lo da forma mais genérica possível. Veja o exemplo abaixo do uso deste Wildcard.
public void processElements(List<?> elements){
for(Object o : elements){
System.out.println(o);
}
}
/* Podemos atribuir um list de qualquer tipo a nosso
método, pois ele tem um tipo desconhecido/genérico */
List<A> listA = new ArrayList<A>();
processElements(listA);
Extends Wildcard
Podemos utilizar este tipo de Wildcard para possibilitar o uso de vários tipos que se relacionam entre si, ou seja, podemos dizer que o nosso método processElements aceita uma lista de qualquer tipo de Frutas, seja moranga, maça ou etc. Vejamos.
public void processElements(List<? extends Fruit> elements){
for(Fruit a : elements){
System.out.println(a.getValue());
}
}
/* Podemos agorar passar nossas frutas diversas ao método processElements */
List<Apple> listApple = new ArrayList<Apple>();
processElements(listApple);
List<Orange> listOrange = new ArrayList<Orange>();
processElements(listOrange);
List<Strawberry> listStrawberry = new ArrayList<Strawberry>();
processElements(listStrawberry);
Super wildcard
Ao contrário do extends, o wildcard super permite que elementos Fruit e Object sejam utilizados, isso significa que apenas são permitidos de “Fruit para cima”. Se fizermos um List estamos permitindo todos os Apples, Fruits e Objects.
Para finalizar, vamos a um último exemplo utilizando Generics e vendo qual será a saída do mesmo:
public class Box<T> {
private T t;
public void add(T t) {
this.t = t;
}
public T get() {
return t;
}
public static void main(String[] args) {
Box<Integer> integerBox = new Box<Integer>();
Box<String> stringBox = new Box<String>();
integerBox.add(new Integer(10));
stringBox.add(new String("Hello World"));
System.out.printf("Integer Value :%d\n\n", integerBox.get());
System.out.printf("String Value :%s\n", stringBox.get());
}
}
Saída deste código:
Integer Value :10
String Value :Hello World
Conclusão
O uso de Generics faz-se necessário para evitar casts excessivos e erros que podem ser encontrados em tempo de compilação, antes mesmo de ir para a produção. Todo profissional da área deve ter o conhecimento de como utilizar este recurso tão poderoso, pois em muito se aumenta a produtividade utilizando-o.