Utilizando Genéricos

Existe a possibilidade de restringir o tipo de dado que entra em um HashSet, por exemplo, a partir de um mecanismo da própria linguagem? A resposta é: Sim.
Porém, essa resposta só veio a partir do Java 5. E essa resposta tem nome, tem semântica e tem sintaxe. É denominada de Genéricos. Observe o fragmento de código a seguir:

    List<Integer> lista = new ArrayList<Integer>();
    lista.add(6); // Sem problemas
    lista.add(new Pai()); // Está errado

Polimorfismo e Genéricos

Os conjuntos genéricos proporcionam, praticamente, os mesmos benefícios da segurança de tipos que você costumava ter com os arrays, entretanto, existem algumas diferenças que são fundamentais e que podem gerar dúvidas, se você não estiver preparado. A maior parte delas vem com o polimorfismo.
Pode-se atribuir um ArrayList a uma referência List, pois List é um supertipo de ArrayList. Essa atribuição polimórfica funciona naturalmente como sempre foi usado em Java, independentemente dos tipos genéricos. Mas e quanto a esse trecho?

    class Animal{ }

class Mamifero extends Animal{ }

List<Animal> lista = new ArrayList<Mamifero>(); // Erro de compilação!


Estamos diante de uma regra bem simples aqui: o tipo da declaração da variável precisa coincidir com o tipo que você passa ao tipo real do objeto. Se for declarado List<Cachorro>,  qualquer coisa que você atribuir à referência dessa lista tem que ser do tipo genérico <Cachorro>.

Observe o código abaixo:

    public static void main(String args[ ]){
            List<Veiculo> dir = new ArrayList<Veiculo>();
            adicionaListaElementos(dir);
            }
            public static void adicionaListaElementos(List<Carro> e){
                e.add(new Carro());
                e.add(new Moto());
                e.add(new Veiculo());

for(Carro x: e)

    System.out.println(x);

}


Este exemplo não compila. Em uma comparação entre os piores erros, obviamente o erro de compilação é um erro menos crítico do que um erro de execução (runtime).
O código não compila, devido, no caso dos genéricos, à obrigatoriedade de compatibilidade entre o tipo do conjunto e o que o método espera, o que, necessariamente, deve ser idêntico. No caso apresentado acima, se o método recebe um List<Carro>, não podemos passar um ArrayList<Veiculo>.

Analise mais uma vez o exemplo:

    package devmedia;
    import java.util.*;

class Numeros{}

class NumerosPrimos extends Numeros{ }

class NumerosInteiros extends Numeros{ }

class NumerosRacionais extends Numeros{ }

public class Teste{

    public static void main(String args[ ]){

        Teste t = new Teste();

        List<Numeros> c = new ArrayList<Numeros>();

        t.adicionarNumero(c);

        System.out.println(c);

    }

    public void adicionarNumero(List<Numeros> a){

        a.add(new NumerosPrimos());

        a.add(new NumerosRacionais());

    }

}


Neste momento, vamos a um exemplo que não realiza nenhuma adição ao conjunto genérico. E que, de fato, há uma forma para dizer ao compilador que se pode usar qualquer subtipo genérico do tipo do argumento declarado, uma vez que não irá colocar nada no conjunto. Esse mecanismo é o coringa<?>.
A assinatura do método sofreria uma alteração de:
    public void adicionarNumero(List<Numeros> a)
para
    public void adicionarNumero(List<? extends Numeros> a)

Nunca se esqueça(e isso é essencial para o exame): ao se usar a notação de coringa, qualquer tipo de conjunto que estenda o que estiver dentro de < > pode ser passado ao método.
Porém, o método não pode adicionar nada! Esse é, realmente, o preço da permissividade polimórfica genérica. Primeiramente, <? extends Veiculo> quer dizer que você pode usar qualquer subtipo de Veiculo. No entanto, esse subtipo pode ser ou uma subclasse de uma classe, seja ela abstrata ou concreta, ou um tipo que implementa alguma interface, que venha após a palavra-chave extends.
Existe também um outro cenário em que o coringa pode ser utilizado. E, inclusive, adicionar ao conjunto, mas de um modo seguro, com a palavra-chave super. Ex.:
    public void adicionarNumero(List<? super NumerosPrimos> a)

Observe um exemplo:

package devmedia;

import java.util.*;

class Animal{ }

class Mamifero extends Animal{ }

class Anfibio extends Animal{ }

class Ave extends Animal{ }

public class TesteGenerico{

    public static void main(String args[ ]){

        TesteGenerico t = new TesteGenerico();

        List<Anfibio> a2 = new ArrayList<Anfibio>();

        List<Animal> a1 = new ArrayList<Animal>();

        List<Object> a3 = new ArrayList<Object>();

        t.adicionaElemento(a1);

        System.out.println(a1);

        t.adicionaElemento(a2);

        System.out.println(a2);

        t.adicionaElemento(a3);

        System.out.println(a3);

    }

    public void adicionaElemento(List<? super Anfibio> a){

        a.add(new Anfibio());

    }

}


No momento em que a sintaxe <? super...> é usada, se diz ao compilador que pode aceitar o tipo que está no lado direito de super ou um dos seus supertipos, uma vez que um conjunto declarado como qualquer supertipo de Anfibio será capaz de aceitar um Anfibio como elemento. List<Object> pode usar um Anfibio. List<Animal> pode usar um Anfibio.

Desse modo, passar qualquer um desses vai funcionar. Assim, a palavra-chave super na notação do coringa permite que tenhamos uma forma restrita, mas também capaz, de adicionar a um conjunto.


Sendo assim, o coringa nos proporciona atribuições polimórficas, mas com algumas restrições que não temos no caso dos arrays. Dúvida: esses dois são idênticos?


    public void fazerAlgumaCoisa(List<Object> lista) { }

           public void fazerAlgumaCoisa(List<?> lista { }


List<?> está usando o coringa sem as palavras-chave super ou extends, ou seja, simplesmente qualquer tipo. Então, poderíamos passar ao método um List<Carro>, List<String>, List<Integer>, List<JButton>, resumindo, qualquer objeto.

E quando se usa o coringa isolado, sem a palavra-chave super, seguida de um tipo, quer dizer que você não pode adicionar nada à lista referida como List<?>. Essa notação não permite adição.

List<?> é totalmente diferente de List<Object>. List<Object> significa que o método só pode usar um List<Object>. Não um List<Casa>, nem um List<Empregado>.

Quer dizer, porém, que pode adicionar à lista, uma vez que o compilador já está certificado de que foi passado apenas um List<Object> válido para o método.


Classes Genéricas


Talvez tenha surgido essa dúvida: será que a especificação de tipos genéricos funciona com outras classes da API? E, por fim, é possível declararmos as nossas próprias classes como tipos genéricos? Resumindo, pode-se criar uma classe que requeira que seja passado um tipo, quando a declararem e instanciarem?

A resposta, que você provavelmente conhece: a API lhe dirá quando um tipo parametrizado for esperado.


Criando sua Classe Genérica


Será criada agora nossa própria classe genérica, para termos uma idéia de como funciona e, depois, serão apresentados alguns detalhes remanescentes sobre a sintaxe dos genéricos. Vamos analisar como fica uma classe genérica <Esquadra>.


package devmedia;

import java.util.*;

class Esquadra<T>{

    private List<T> l;

    Frota(List<T> lista){

        l = lista;

    }

    public List<T> getLista(){

        return l;

    }

}

class Moto{

    Integer chassi;

    String modelo;

    Moto(Integer c, String m){

        chassi = c;

        modelo = m;

    }

    public String toString(){

        return chassi + “ ” + modelo + “ “;

    }

}

public class TesteGeneric{

    public static void main(String args[ ]){

        Moto m1 = new Moto(12, “GM”);

        Moto m2 = new Moto(23, “Ford”);

        List<Moto> l1 = new ArrayList<Moto>();

        l1.add(m1);

        l1.add(m2);

        Esquadra<Moto> e = new Esquadra<Moto>(l1);

        System.out.println(e.getLista());

    }

}


A classe Esquadra<T> aceita um tipo genérico <T>, onde T pode ser, simplesmente, qualquer coisa. É por esse motivo que a classe é uma classe genérica. Esse tipo será utilizado para ser o tipo do conjunto. Devido a isso, na sequência foi criada a classe Moto. A esquadra será composta por um conjunto de motos. Perceba, na classe de teste (TesteGeneric), a sintaxe para instanciarmos um objeto Esquadra e. O construtor da classe, que aceita um List, tem que ser invocado de forma plenamente compatível.

Criando Métodos Genéricos

Há a possibilidade de definirmos um tipo parametrizado em um nível mais específico, o do método. Esse primeiro exemplo de método genérico aceita dois argumentos: uma Collection qualquer e um array qualquer. A finalidade desse método é ler o array e transformá-lo em uma Collection. É um método bem versátil e útil.

package devmedia;

import java.util.*;

public class MethodTest

{

    public static<T> void fromArrayToCollection(T[ ] a, Collection c)

    {

        for(T o : a)

        {

            c.add(o);

        }

    }

    public static void main(String args[ ])

    {

        Double[ ] doubleArray = { 1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7 };

        List lista = new ArrayList();

        fromArrayToCollection(doubleArray, lista);

        System.out.println(lista);


        lista.removeAll(lista);


        String [ ] nomes = {“Java”, “Generics”, “Conjunto” };

        fromArrayToCollection(nomes, lista);

        System.out.println(lista);

    }

}


Saída:

[1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7]
[Java, Generics, Conjunto]

Suponha que seja preciso criar um método que use uma instância de qualquer tipo e mostre os elementos de um array de qualquer tipo. A classe não precisa ser genérica; basicamente, precisamos apenas de um método utilitário ao qual possamos passar um tipo e que seja capaz de usar esse tipo para construir um conjunto com tipo específico. Utilizando um método genérico, pode-se declarar o método sem um tipo específico e posteriormente obter a informação do tipo com base no tipo do objeto passado ao método como parâmetro.

Mini-teste.

1. Analise o seguinte trecho de código:

package devmedia;
import java.util.*;
public class Teste1{
    static void inserir(List lista){
        int total = 0;
        for(Object x : lista)
            total += (Integer)x;
        System.out.println(total);
    }
    public static void main(String args[ ]){
        List<Integer> lista = new ArrayList<Integer>();
        lista.add(10);
        lista.add(new Integer(5));
        lista.add(new Integer(5) - 2);
        inserir(lista);
    }
}

Qual das alternativas a seguir representa a saída do código acima?

a) 15
b) 18
c) O código apresentará erro de compilação.
d) Será lançada uma exceção.

2. Analise o seguinte trecho de código:

package devmedia;
import java.util.*;
class Moto{
    String marca;
    Moto(String marca){
        this.marca = marca;
    }
    public String toString(){
        return marca;
    }
}
public class Executavel{
    static void adicionar(List l){
        l.add(1);
        l.add(new Integer(10));
        l.add(new Moto(“GM”));
    }
    public static void main(String args[ ]){
        List<String> lista = new ArrayList<String>();
        lista.add(“Honda”);
        adicionar(lista);
        System.out.println(lista);
    }
}

Qual das alternativas a seguir representa a saída do código acima?

a) 1 10 GM Honda
b) Honda 1 10 GM
c) O código apresentará erro de compilação.
d) Será lançada uma ClassCastException.

3. Analise o seguinte trecho de código:

package devmedia;
import java.util.*;
class Latim{}
class Portugues extends Latim{}
public class Teste{
    public static void main(String args[ ]){
        Latim [ ] a = new Latim[10]; //1
        Portugues [ ] b = new Portugues[20]; //2
        a = b; //3
        a = new Portugues[30]; //4
        b = new Latim[10]; //5
        Object o = a; //6
    }
}

Qual das alternativas a seguir representa o que acontecerá?

a) O código compilará sem problemas.
b) O código não compila devido a um erro na linha com comentário 1.
c) O código não compila devido a um erro na linha com comentário 2.
d) O código não compila devido a um erro na linha com comentário 3.
e) O código não compila devido a um erro na linha com comentário 4.
f) O código não compila devido a um erro na linha com comentário 5.
g) O código não compila devido a um erro na linha com comentário 6.

4. Analise o seguinte trecho de código:

package devmedia;
import java.util.*;

interface Recebivel{}
class Empregado implements Recebivel{}
class NotaFiscal implements Recebivel{}
class Imposto implements Recebivel{}
public class Teste{
    static void receber(List<? extends Recebivel> x){
        x.add(new NotaFiscal());
        x.add(new Imposto());
        x.add(new Empregado());
    }
    public static void main(String args[ ]){
        List<Empregado> l1 = new ArrayList<Empregado>();
        List<NotaFiscal> l2 = new ArrayList<NotaFiscal>();
        List<Imposto> l3 = new ArrayList<Imposto>();

    receber(l1); //1

    receber(l2); //2

    receber(l3); //3

}

}

Qual das alternativas a seguir representa o que acontecerá?

a) O programa não compila devido a um erro na linha com comentário 1.
b) O programa não compila devido a um erro na linha com comentário 2.
c) O programa não compila devido a um erro na linha com comentário 3.
d) O programa não compila devido à tentativa de inserção de receber().
e) Será lançada uma exceção.
f) O código compilará sem problemas.

5. Analise o seguinte trecho de código:

package devmedia;
import java.util.*;
class Animal{}
class Mamifero extends Animal{}
class Primata extends Mamifero{}
class Macaco extends Primata{}
public class Executavel{
    static void aceitar(List<? super Macaco> x){
        x.add(new Macaco()); //1
        x.add(new Primata()); //2
        x.add(new Mamifero()); //3
        x.add(new Animal()); //4
    }
    public static void main(String args[ ]){
        List<Macaco> l1 = new ArrayList<Macaco>();
        List<Primata> l2 = new ArrayList<Primata>();
        List<Mamifero> l3 = new ArrayList<Mamifero>();
        List<Animal> l4 = new ArrayList<Animal>();
        aceitar(l1);
        aceitar(l2);
        aceitar(l3);
        aceitar(l4);
    }
}

Qual(is) item(ns) a seguir representa(m) o que acontecerá?

a) O programa não compila devido a um erro na linha com comentário 1.
b) O programa não compila devido a um erro na linha com comentário 2.
c) O programa não compila devido a um erro na linha com comentário 3.
d) O programa não compila devido a um erro na linha com comentário 4.
e) Será lançada uma exceção.


Gabarito comentado

1. Resposta correta: B
Essa questão analisa a convivência entre código mais novo com código pré-genérico. O código não possui problemas, pois o conjunto que é passado ao método inserir() é uma lista numérica. Entretanto, o método inserir(), por ser um método anterior aos Generics, aceita um conjunto contendo quaisquer referências a objetos, enfim, não há restrições. Questões dessa forma requerem atenção e também alguma habilidade para que se chegue à conclusão de que não há problemas de conversibilidade.

2. Resposta correta: B
Esse código é , realmente, um pouco confuso. Tal confusão se origina da mescla de códigos novo e antigo. Embora o ArrayList seja um conjunto de Strings, devido à mescla ele aceita elementos que não são Strings. Este código compila e executa. Como um ArrayList é ordenado pelo índice, os elementos são mostrados na mesma ordem em que foram adicionados.
Não existe manipulação que tenha necessidade de um tipo específico no código, como uma soma, por exemplo, que obrigaria os elementos a serem números, ou conversíveis para números, ou seja, não temos problemas de conversão.

3. Resposta correta: F
Partindo-se da premissa que Portugues é um Latim, um array Portugues é um array Latim também. Do mesmo modo que uma referência a um objeto de uma subclasse pode ser atribuída a uma referência de uma superclasse, um array-filho pode ser atribuído a um array-pai. Entretanto, o oposto não é possível. Um Animal não é um Gato. Um Empregado não é um Diretor. Um array-pai não é um array-filho. Sendo assim, o que invalida o código, impedindo-o de compilar, é a linha com comentário 5. Outra questão interessante a ser citada é que é possível atribuir uma referência array a um Object, não importando, inclusive, a dimensionalidade do array.

4. Resposta correta: D
O programa não compila porque a sintaxe receber(List<? extends Recebivel> x) não permite a inserção de nada ao conjunto. Se o método receber() tentar adicionar algo, o programa não compilará. Isso acontece por causa da homogeneidade dos conjuntos, uma das principais características dos genéricos. A homogeneidade dos elementos é garantida pela incapacidade de inserção.

5. Resposta correta: B, C e D
A sintaxe aceitar(List<? super Macaco> x) permite inserções nos conjuntos passados ao método. Porém, apenas elementos da classe Macaco podem ser inseridos. Isso está certo porque, independentemente do que você passe como argumento a esse método, um Macaco poderá fazer parte sempre.

Leia todos artigos da série