Introdução aos Generics
Por Francisco Demontiê dos Santos Junior
(demontie@dsc.ufcg.edu.br)
A Programação Genérica é um paradigma a partir do qual os algoritmos são escritos de forma independente do tipo de dados. Isso se dá por meio de uma gramática estendida que adiciona alguma construção que irá se adaptar a partir de definições da instância do algoritmo. Os Generics surgiram para incorporar esse paradigma à linguagem de programação Java.
e Leonardo Alves dos Santos
(santos.leonardoalves@gmail.com)

O conceito de Generics surgiu na década de 1970 com as linguagens Ada e Clu. Depois disso, outras linguagens implementaram esse conceito, porém, ele se difundiu com o surgimento dos templates C++. Em Java, esse conceito foi implementado na versão 1.5, versão que proporcionou grandes mudanças na linguagem. Mas, o que são exatamente os generics? Generics são estruturas que permitem a escrita de um algoritmo sem a preocupação com os tipos de dados.

O uso dos Generics Java é bastante simples (em sua sintaxe). Para criar uma classe que possui objetos de um tipo genérico, basta acrescentar à declaração dessa uma ou mais variáveis de tipo (separadas por vírgulas). Essas variáveis são identificadores que serão substituídos por tipos escolhidos pelo programador. Elas fornecem informações em tempo de compilação e de execução. Na Listagem 1 é apresentado um exemplo simples de uso dos Generics.

public class HelloWorldGenerics<T> {

	T obj;
	
	public void setObj(T obj){
		this.obj = obj;
	}
	
	public String getMessage(){
		return "Objeto: " + obj.toString() + "; " + obj.getClass();
	}
	
	public static void main(String[] args){
		HelloWorldGenerics<Integer> a = new HelloWorldGenerics<Integer>();
		a.setObj(2);
		System.out.println(a.getMessage());
		HelloWorldGenerics<String> b = new HelloWorldGenerics<String>();
		b.setObj("2");
		System.out.println(b.getMessage());
	}
	
}

Perceba que, caso o método setObj seja chamado com um parâmetro de um outro tipo que não seja Integer, o compilador acusará um erro. É dessa forma que foram implementadas as coleções de Java. No caso da interface Map, utiliza-se duas variáveis de tipo, uma é o tipo da chave e a outra o tipo dos valores contidos no mapa.

Em alguns casos, podemos precisar restringir os tipos de objetos com os quais trabalharemos. Para tal, podemos utilizar o conceito de sub tipos nos generics. Utilizamos, então, a palavra reservada extends (aqui, extends, não é referente apenas à subtipagem a partir da herança, mas também por meio de interfaces). Suponha, então, que queremos construir um algoritmo de ordenação genérico (utilizaremos, para este exemplo, o "insertion sort") e, por isso, temos que trabalhar apenas com objetos que sejam comparáveis. Veja a Listagem 2.

import java.util.Arrays;

public class UtilizandoGenerics {
	
	public static <T extends Comparable<T>> void selectionSort(T[] v) {
		int index_min;
		T aux;

		for (int i = 0; i < v.length; i++) {
			index_min = i;
			for (int j = i + 1; j < v.length; j++) {
				if (v[j].compareTo(v[index_min]) < 0) {
					index_min = j;
				}
			}
			if (index_min != i) {
				aux = v[index_min];
				v[index_min] = v[i];
				v[i] = aux;
			}
		}
	}
	
	public static void main(String[] args) {
		Integer[] vetor = new Integer[]{2,4,6,1,3,9,5};
		selectionSort(vetor);
		System.out.println(Arrays.toString(vetor));
		String[] vetor2 = new String[]{"b","d","f","a","c","i","e"};
		selectionSort(vetor2);
		System.out.println(Arrays.toString(vetor2));
	}
	
}

Nesse exemplo, duas coisas merecem um comentário. A primeira delas é que nossa variável de tipo T será substituída, obrigatoriamente, por alguma classe que implemente a interface Comparable. Isso nos garante que poderemos sempre comparar os elementos do array. A segunda coisa que merece destaque é o fato de a variável de tipo ter sido definida na assinatura do método, e não na assinatura da classe. Chamamos isso de "Generic Methods". Podemos nos perguntar, então, qual a vantagem de se usar esse tipo de método se poderíamos, apenas, definir o tipo de parâmetro do método por Comparable[ ] (array de Comparable). Nesse caso, funcionaria normalmente, porém, observe a Listagem 3.

	public static <T extends Comparable<T>> T min(List<T> l){
		T min = l.get(0);
		for (int i=1; i < l.size(); i++)
			if (l.get(i).compareTo(min) < 0)
				min = l.get(i);
		return min;
	}
	
	public static Comparable max(List<Comparable> l){
		Comparable max = l.get(0);
		for (int i=1; i < l.size(); i++)
			if (l.get(i).compareTo(max) > 0)
				max = l.get(i);
		return max;
	}
	
	public static void main(String[] args) {
		List<Integer> l = new ArrayList<Integer>();
		l.add(2);
		l.add(7);
		l.add(-1);
		l.add(3);
		System.out.println("O menor elemento da lista eh: " + min(l));
		System.out.println("O maior elemento da lista eh: " + max(l));
	}

Nesse caso, a função min invocada com parâmetro do tipo List irá funcionar normalmente, enquanto a invocação da função max com o mesmo parâmetro resultará num erro em tempo de compilação. Para resolver o problema da compilação, podemos definir o tipo de parâmetro da função max como sendo List, em que a "?" é uma Wild Card ("carta coringa"). Podemos ler "uma lista de qualquer coisa que seja um sub tipo de Comparable". Mesmo assim, o tipo de retorno da função continuará sendo Comparable, e não o tipo de elementos contidos na coleção (como geralmente desejamos).

Algumas observações:

  • Podemos, ainda, definir mais de um super tipo para uma variável de tipo. Para isto, basta utilizar o operador "&" (por exemplo, "class Aextends Cloneable & Comparable>").
  • O compilador não consegue realizar certas "mágicas" com o uso de generics, a exemplo da criação de arrays de tipo genérico (T[] v = new T[10];). Caso tentemos isto, seremos contemplados com um erro de compilação. Uma alternativa a isso é criarmos um array de Object e realizarmos um cast para o tipo genérico (T[] v = (T[])new Object[10];). Nesse caso, receberemos um warning, pois Java não consegue solucionar esse tipo de cast (mesmo que, para nós, ele seja seguro).

A Programação Genérica pode ser bastante poderosa e útil, e os Generics Java são uma forma bastante simples de se programar sob os conceitos de tal paradigma. Porém, é preciso ter cuidado com certas operações e saber escolher bem a família de tipos sob as quais seus algoritmos devem atuar.

Fontes:

[1] http://pt.wikipedia.org/wiki/Programa%C3%A7%C3%A3o_gen%C3%A9rica. Último acesso em 22/06/2011.

[2] http://java.sun.com/j2se/1.5/pdf/generics-tutorial.pdf. Último acesso em 22/06/2011.