Generics em Java: para que servem e como usar
Generics (genéricos) — um dos recursos principais da linguagem Java, introduzido na versão J2SE 5.0. Eles permitem criar classes, interfaces e métodos que trabalham com diferentes tipos de dados, mantendo a tipagem estrita em tempo de compilação. Neste artigo, vamos entender para que servem os genéricos, como usá-los corretamente e quais erros eles ajudam a evitar.
Para que servem os Generics em Java?
Antes do surgimento dos genéricos, os programadores usavam casting (conversão de tipos) e trabalhavam com Object. Isso gerava dois problemas principais:
- Falta de type safety — qualquer tipo de objeto podia ser adicionado a uma coleção, e o erro só aparecia em tempo de execução.
- Necessidade de casting explícito — ao recuperar um elemento da coleção, era necessário fazer um cast, o que poluía o código.
// Sem Generics (Java anterior ao 5)List list = new ArrayList();list.add("string");list.add(123); // Erro não ocorre em tempo de compilaçãoString s = (String) list.get(0); // É necessário fazer cast explícito// Com GenericsList<String> list = new ArrayList<>();list.add("string");// list.add(123); // Erro de compilação!String s = list.get(0); // Cast não é necessárioPrincipais vantagens dos Generics:
- Type safety — erros de tipo são detectados em tempo de compilação.
- Eliminação de casts explícitos — o código fica mais limpo e legível.
- Reutilização de código — uma única implementação funciona com diferentes tipos.
Criação de classes e métodos genéricos
Classe genérica
Para criar uma classe que trabalha com um tipo arbitrário, basta especificar o parâmetro de tipo entre colchetes angulares após o nome da classe:
public class Box<T> { private T content;
public void setContent(T content) { this.content = content; }
public T getContent() { return content; }
public static void main(String[] args) { Box<String> stringBox = new Box<>(); stringBox.setContent("Olá, Java!"); System.out.println(stringBox.getContent());
Box<Integer> intBox = new Box<>(); intBox.setContent(42); System.out.println(intBox.getContent()); }}Neste exemplo, T é o parâmetro de tipo. Ao criar o objeto Box<String>, todas as ocorrências de T são substituídas por String.
Método genérico
Métodos também podem ser parametrizados. O parâmetro de tipo é especificado antes do tipo de retorno:
public class Util { public static <T> void printArray(T[] array) { for (T element : array) { System.out.print(element + " "); } System.out.println(); }
public static void main(String[] args) { Integer[] intArray = {1, 2, 3, 4, 5}; String[] strArray = {"A", "B", "C"};
printArray(intArray); // 1 2 3 4 5 printArray(strArray); // A B C }}Observe: o parâmetro de tipo <T> vem antes de void. Isso é obrigatório para métodos genéricos.
Tipos limitados (Bounded Type Parameters)
Às vezes é necessário restringir quais tipos podem ser usados como parâmetro. Para isso, utilizam-se as palavras-chave extends e super.
Limite superior (extends)
Restringe o tipo por cima — permite usar apenas a própria classe especificada ou suas subclasses:
public class Calculator<T extends Number> { private T value;
public Calculator(T value) { this.value = value; }
public double square() { return va