Generics em Java

Generics foram introduzidos no Java 5 para fornecer segurança de tipo em tempo de compilação e eliminar a necessidade de conversões explícitas. Eles permitem que você defina classes, interfaces e métodos com tipos parametrizados, proporcionando um código mais reutilizável e robusto. Neste artigo, exploraremos como usar generics em Java, cobrindo desde os conceitos básicos até usos avançados como wildcards.

Subtipos

Em Java, a subtipagem com generics não funciona exatamente como se poderia esperar. Por exemplo, List<String> não é um subtipo de List<Object>. Isso ocorre porque, ao permitir isso, a segurança de tipo seria comprometida. Veja um exemplo:

List<String> stringList = new ArrayList<>();
// List<Object> objList = stringList; // Isso não compila

Entretanto, arrays genéricos seguem uma lógica diferente:

Object[] objArray = new String[10]; // Isso compila

No entanto, esse comportamento pode levar a exceções em tempo de execução:

Object[] objArray = new String[10];
objArray[0] = 10; // Lança ArrayStoreException em tempo de execução

Usando Generics em Classes e Interfaces

Definir uma classe ou interface genérica é bastante simples. Aqui está um exemplo de uma classe genérica:

public class Caixa<T> {
    private T conteudo;

    public void setConteudo(T conteudo) {
        this.conteudo = conteudo;
    }

    public T getConteudo() {
        return conteudo;
    }
}

Você pode usar esta classe com diferentes tipos de dados:

Caixa<String> caixaString = new Caixa<>();
caixaString.setConteudo("Olá, Mundo!");
System.out.println(caixaString.getConteudo());

Caixa<Integer> caixaInteger = new Caixa<>();
caixaInteger.setConteudo(123);
System.out.println(caixaInteger.getConteudo());

Da mesma forma, podemos criar interfaces genéricas:

public interface Par<K, V> {
    K getChave();
    V getValor();
}

Usando Generics em Métodos e Construtores

Além de classes e interfaces, métodos também podem ser genéricos. Aqui está um exemplo de um método genérico:

public class Util {
    public static <T> void imprimirArray(T[] array) {
        for (T elemento : array) {
            System.out.print(elemento + " ");
        }
        System.out.println();
    }
}

Você pode chamar este método com arrays de diferentes tipos:

Integer[] arrayInt = {1, 2, 3, 4, 5};
String[] arrayStr = {"A", "B", "C", "D"};

Util.imprimirArray(arrayInt);
Util.imprimirArray(arrayStr);

Construtores genéricos também são possíveis, embora menos comuns:

public class Par<K, V> {
    private K chave;
    private V valor;

    public <T> Par(T chave, T valor) {
        this.chave = (K) chave;
        this.valor = (V) valor;
    }

    // Getters...
}

Wildcards

Wildcards (?) são usados em generics para representar um tipo desconhecido. Existem três tipos principais de wildcards:

Wildcard não restrito

Representa qualquer tipo:

public void imprimirLista(List<?> lista) {
    for (Object elem : lista) {
        System.out.print(elem + " ");
    }
    System.out.println();
}

Wildcard com restrição superior

Restringe o tipo a ser um subtipo de um tipo específico:

public void imprimirListaNumeros(List<? extends Number> lista) {
    for (Number num : lista) {
        System.out.print(num + " ");
    }
    System.out.println();
}

Wildcard com restrição inferior

Restringe o tipo a ser um supertipo de um tipo específico:

public void adicionarNumeros(List<? super Integer> lista) {
    lista.add(1);
    lista.add(2);
    lista.add(3);
}

Conclusão

Generics são uma poderosa funcionalidade em Java, proporcionando maior flexibilidade e segurança de tipo. Compreender como e quando usar generics pode melhorar significativamente a qualidade do seu código. Desde a definição de classes e métodos genéricos até o uso de wildcards, generics oferecem uma maneira eficiente de lidar com tipos em Java. Experimente aplicar generics em seus projetos para aproveitar ao máximo esses benefícios.