Na primeira parte deste artigo, para introduzir o leitor aos
principais conceitos sobre Generics, criamos um aplicativo simples para
gerenciar estacionamentos. No sistema, diversos tipos de veículos poderiam ser
armazenados, como carros, motocicletas e ônibus. Com o objetivo de demonstrar a
possibilidade de inclusão de objetos diferentes de Veiculo
no estabelecimento, desenvolvemos a classe Cacamba e
cadastramos no estacionamento um objeto deste tipo.
Entretanto, para listar os veículos cadastrados em Estacionamento
(por meio do método listarVeiculos()), é
necessária a conversão de todos os objetos armazenados no atributo veiculos
para Veiculo.
Precisamos fazer a troca de um tipo para outro porque os objetos gerenciados
por veiculos
são implicitamente do tipo Object.
Assim, no momento da leitura dos dados referentes ao
elemento Cacamba,
devidamente cadastrado em veiculos, uma
exceção será lançada na tentativa de conversão deste objeto (ainda com o tipo
Object) para Veiculo. A troca de
tipos deste elemento provoca uma ClassCastException –
exceção lançada ao tentar converter um objeto para um tipo diferente do seu. Neste
caso, a troca do tipo Object deveria ser
para Cacamba,
ao invés de Veiculo.
Nesta segunda parte, apresentaremos Generics e mostraremos
como utilizar este poderoso recurso para resolver os problemas apontados na edição
anterior e resumidos acima.
Conforme mencionado na parte 1, Generics introduz a
verificação, em tempo de compilação, dos tipos manipulados por uma classe. Por
exemplo, podemos ter um objeto List gerenciando um
elemento do tipo String, e outro controlando, por
exemplo, um StringBuffer. Os tipos gerenciados por
uma classe são definidos, em Generics, como tipos parametrizados. Neste
exemplo, String
e StringBuffer
são definidos como os tipos parametrizados dos seus respectivos objetos List.
Mais detalhes sobre este assunto serão discutidos
ao longo da matéria.
Ademais, mostraremos como o uso de Generics reflete na
redução de bugs em tempo de execução, empregando a verificação dos tipos
gerenciados por uma classe. Apresentaremos também como, a partir da criação dos
nossos próprios tipos genéricos, podemos utilizar e reutilizar estas classes
genéricas para a criação de objetos com seus respectivos tipos parametrizados. Por
exemplo, podemos ter um objeto desta classe genérica gerenciando objetos Integer
e outro controlando elementos do tipo Double. Por fim, a
aplicação do polimorfismo aos tipos parametrizados também será abordada.
Introdução ao Generics para solucionar o problema
O Generics foi especificado na JSR 014. Na criação desta
JSR, um dos objetivos para adicionar este recurso foi incluir verificadores,
conhecidos como tipos parametrizados, para explicitar os elementos que podem
ser manipulados por uma classe. Por exemplo, podemos declarar um objeto List
para fazer operações somente com Strings. Na prática,
para aplicar o recurso, após a declaração de List
colocamos entre chaves (< >) o tipo String.
A Listagem 1
reflete esta situação. Criamos o objeto listaCidades, do
tipo List, para armazenar nomes de cidades. Ao instanciar
este elemento, é necessária a declaração de uma String
entre as chaves, como já mencionado anteriormente, como o tipo parametrizado
deste elemento. Ainda com o objetivo de demonstrar o uso de Generics, foram criados
métodos para inclusão e listagem dos elementos da lista.
Listagem 1. AplicacaoListGenerica.java:
exemplifica a criação de uma lista para manipulação somente de objetos do tipo
String.
package
br.com.devmedia.javamagazine.exemplolistgenerics;
import java.util.List;
import java.util.ArrayList;
public class AplicacaoListGenerica {
//lista com
tipo parametrizado String
private
List<String> listaCidades;
public
static void main(String args[]) {
AplicacaoListGenerica exemploListGenerica = new AplicacaoListGenerica();
exemploListGenerica.criarListaCidades();
exemploListGenerica.inserirCidades();
}
public AplicacaoListGenerica() {
listaCidades = new
ArrayList<String>();
}
//insere
nome de cidades em listaCidades
private void inserirCidades() {
listaCidades.add("São Paulo");
listaCidades.add("Belo Horizonte");
listaCidades.add("Brasília");
}
//lista o
nome das cidades armazenadas no objeto listaCidades
private void listarCidades() {
for (String cidade: listaCidades) {
System.out.println("Nome
da cidade = " + cidade);
}
}
}
Mas, e se desejássemos incluir um objeto diferente de String na lista? Para exemplificar, vamos fazer a tentativa
de incluir uma variável do tipo char
(convertida para Character utilizando autoboxing) por meio do trecho de código adicionado
ao método inserirCidades(), referente
à Listagem 1, e apresentado a seguir:
private void inserirCidades() {
//inserção das
cidades
.
.
//tentativa de
inserção de um tipo char na lista
listaCidades.add('S');
}
Na Figura 1,
observamos que não é possível a inserção de um objeto do tipo Character
na lista. Caso exista uma tentativa de inserir um objeto diferente de String em listaCidades,
uma exceção será lançada ainda em tempo de compilação. Com a ocorrência deste
erro antes da execução, o programa torna-se mais seguro, prevenindo a inserção
de uma variável diferente de uma String na lista.
Figura 1. Resultado da execução da
classe AplicacaoListGenerica.java.
Dentro deste contexto, concluímos que um dos principais
motivos para a utilização de Generics é a garantia que somente o tipo genérico
declarado na instanciação de um objeto pode ser manipulado. Ademais, caso haja
a tentativa de uso de um tipo de objeto diferente daquele que foi declarado,
exceções serão lançadas no momento da compilação do código fonte. Dessa forma, o
programa torna-se mais confiável em tempo de execução, pois não há riscos de
ocorrerem exceções como uma ClassCastException, citada
anteriormente.
Aplicando Generics ao nosso problema
Após compreendermos como funciona Generics na prática, vamos aplicá-lo na solução do problema de
inclusão de objetos diferentes de Veiculo em
um Estacionamento.
Para limitarmos nossa lista de veículos, modificaremos a Listagem 2 e adicionaremos o recurso
para a inclusão somente de objetos do tipo Veiculo
no estacionamento. Na Listagem 3, aplicamos
as modificações necessárias para a adaptação da classe em questão.
No momento da criação da lista, devemos declarar entre as
chaves que esta irá trabalhar somente com objetos do tipo Veiculo
(de acordo com a definição de Generics, esta classe será o tipo parametrizado
do objeto List
declarado).
Alteramos também os métodos adicionar()
e listarVeiculos(), de forma que estes
recebam e gerenciem objetos do tipo Veiculo e outros da sua
hierarquia. Com isso, limitamos estas operações para trabalhar somente com os
tipos de objetos em questão. Consequentemente, isso evita a
inclusão de elementos diferentes e elimina a necessidade de realizar uma
conversão explícita para a leitura dos seus dados. Por exemplo, no método listarVeiculos()
não é mais necessária a conversão para Veiculo porque os
objetos armazenados em veiculos já estão
explicitados com o tipo em questão.
Listagem 2.
Estacionamento.java: representa o local de armazenamento de veículos.
package
br.com.devmedia.javamagazine.generics.estacionamento.semgenerics;
import java.util.ArrayList;
import java.util.List;
import
br.com.devmedia.javamagazine.generics.estacionamento.veiculos.Veiculo;
public class Estacionamento {
private String
nome;
private String
localizacao;
//lista dos
veículos a serem armazenados no local
private List
veiculos;
public Estacionamento()
{
veiculos = new
ArrayList();
}
public void
adicionar(Object o) {
veiculos.add(o);
System.out.println("Adicionado ao estacionamento
= " + o.toString() + "\n");
}
//lista os
veículos armazenados no estacionamento
public void
listarVeiculos() {
for (Object o:
veiculos) {
Veiculo veiculo
= (Veiculo)o;
System.out.println(veiculo.listarDados());
}
}
//getters e setters default
}
Listagem 3. Estacionamento.java:
Classe definida com Generics para manipulação somente de objetos do tipo
Veiculo.
package
br.com.devmedia.javamagazine.generics.estacionamento.comgenerics;
import java.util.ArrayList;
import java.util.List;
import
br.com.devmedia.javamagazine.generics.estacionamento.veiculos.Veiculo;
public class Estacionamento {
private String nome;
private String localizacao;
//Lista parametrizada para realizar operações
SOMENTE com objetos do tipo Veiculo.
private List<Veiculo> veiculos;
public Estacionamento() {
veiculos = new ArrayList<Veiculo>();
}
//Com
Generics, devemos inserir SOMENTE objetos da hierarquia de Veiculo
public void adicionar(Veiculo veiculo) {
veiculos.add(veiculo);
}
//Com
Generics, SOMENTE objetos do tipo Veiculo terão seus dados listados
public void listarVeiculos() {
for (Veiculo veiculo: veiculos) {
veiculo.listarDados();
}
}
}
Executando a Listagem
3, originada da alteração da Listagem
2, observamos o seguinte resultado ao compilar o código da Listagem 4, no método adicionarCacamba():
Exception
in thread "main" java.lang.Error: Unresolved compilation problem:
The method adicionar(Veiculo) in
the type Estacionamento is not applicable for the arguments (Cacamba)
at ..
De acordo com a mensagem acima, ao aplicar Generics, adicionamos,
em tempo de compilação, uma verificação dos tipos a serem manipulados pela
classe em questão. Ou seja, a classe referente à Listagem 3 não irá compilar caso um objeto diferente de Veiculo
seja inserido em Estacionamento.
Listagem 4. Código
da classe GerenciadorEstacionamento: simula as funções de um sistema de
gerenciamento de estacionamento.
package
br.com.devmedia.javamagazine.generics.estacionamento;
import java.util.ArrayList;
import java.util.List;
import
br.com.devmedia.javamagazine.generics.estacionamento.cacamba.Cacamba;
import
br.com.devmedia.javamagazine.generics.estacionamento.semgenerics.Estacionamento;
import
br.com.devmedia.javamagazine.generics.estacionamento.veiculos.Carro;
import
br.com.devmedia.javamagazine.generics.estacionamento.veiculos.Motocicleta;
import
br.com.devmedia.javamagazine.generics.estacionamento.veiculos.Onibus;
public class GerenciadorEstacionamento {
Estacionamento
estacionamento;
public static void main(String args[]) {
GerenciadorEstacionamento gerenciador = new GerenciadorEstacionamento();
gerenciador.gerenciarEstacionamento();
}
public void gerenciarEstacionamento() {
estacionamento
= new Estacionamento();
adicionarVeiculos();
listarVeiculos();
}
private void adicionarVeiculos() {
Carro gol
= new Carro("Gol",
"Volkswagen", "Azul");
Carro
vectra = new
Carro("Vectra", "Chevrolet", "Preto");
Motocicleta
ninja = new
Motocicleta("Ninja", "Kawasaki", "Verde");
Motocicleta
burgman = new
Motocicleta("Burgman", "Suzuki", "Cinza");
Onibus scania = new Onibus("Scania", "Scania",
"Prata");
estacionamento.adicionar(gol);
estacionamento.adicionar(vectra);
estacionamento.adicionar(ninja);
estacionamento.adicionar(burgman);
estacionamento.adicionar(scania);
}
private void adicionarCacamba() {
Cacamba cacamba
= new Cacamba(5.0d, 7.3d, 4.5d,
"Prata");
estacionamento.adicionar(cacamba);
}
private void listarVeiculos() {
estacionamento.listarVeiculos();
}
//setters e
getters default
}
Execução das classes após a verificação dos tipos
Após a verificação dos tipos parametrizados, e se não ocorrer nenhum
problema antes da execução, estes são removidos pelo compilador. Como a verificação
dos elementos é feita antes de iniciarmos o programa, as classes são executadas
SEM os tipos parametrizados. Agora você deve estar pensando: mas como assim são
removidos?
É isso mesmo! Após a verificação dos tipos em tempo de
compilação e não ocorrendo nenhum erro, o código com verificadores é traduzido
para um código semelhante ao da versão 1.4 do Java. Ou seja, a declaração de um
List com um tipo parametrizado conforme o
código:
List<Veiculo> veículos = new
ArrayList<Veiculo>();
É executado da seguinte maneira:
List veículos = new ArrayList();
Como o uso dos tipos parametrizados reflete na segurança dos
elementos, na compilação já garantimos que somente objetos do tipo Veiculo
são gerenciados por veiculos. Por este
motivo, não há a necessidade de aplicar este conceito em tempo de execução.
Outro ponto importante de Generics é a aplicação do conceito
de polimorfismo. Como o objeto veiculos é do tipo List<Veiculo>, ao executar o método adicionarVeiculos() da Listagem 4, podemos inserir também objetos da hierarquia de Veiculo (Carro, Motocicleta e Ônibus).
Entretanto, ainda existe um caso que merece mais atenção: polimorfismo aplicado
aos tipos parametrizados.
Aplicação de coringas (Wildcards) em tipo genéricos
Para exemplificar a aplicação de coringas e o porquê do seu
uso nos tipos genéricos, vamos adicionar o método remover(),
apresentado a seguir, na Listagem 3:
public void remover(List<Veiculo>
veiculosParaRemocao) {
veiculos.removeAll(veiculosParaRemocao);
for (Veiculo veiculoRemovido:
veiculosParaRemocao) {
System.out.println(veiculoRemovido.toString());
}
}
Este recebe um objeto
do tipo List<Veiculo>, indicando um
conjunto de veículos a serem removidos do objeto Estacionamento.
Agora observe o método removerVeiculos()
abaixo, adicionado à classe GerenciadorEstacionamento:
private void removerVeiculos() {
List<Carro> carrosParaRemover = new ArrayList<Carro>();
Carro gol = new Carro("Gol", "Volkswagen",
"Azul");
Carro vectra = new Carro("Vectra",
"Chevrolet", "Preto");
carrosParaRemover.add(gol);
carrosParaRemover.add(vectra);
estacionamento.remover(carrosParaRemover);
}
Nesse método, criamos um objeto do tipo List<Carro> com os carros a serem removidos
do sistema. Ao ser passado como parâmetro para o método listar(), é esperada a remoção com sucesso de Estacionamento.
Entretanto, o inesperado
ocorre:
Exception in thread "main"
java.lang.Error: Unresolved compilation problem:
The
method remover(List<Veiculo>) in the type Estacionamento is not
applicable for the arguments (List<Carro>)
Essa falha acontece devido a uma restrição de Generics. Com
esse recurso não podemos fazer a mesma associação utilizada no polimorfismo. Neste,
podemos associar o subtipo ao seu supertipo (mais conhecida como conversão
ampliadora) sem perder as suas principais características. Por exemplo, podemos
associar um objeto Carro a uma variável
do tipo Veiculo,
não perdendo as principais funcionalidades e atributos. Consequentemente, enquanto
a conversão abaixo é válida:
Carro gol = new Carro("Gol", "Volkswagen",
"Azul");
Veiculo
veiculo = gol;
Utilizando a mesma analogia com Generics, não é válida a
aplicação de polimorfismo ao tentar converter uma lista com elementos de um
subtipo para uma lista com elementos do seu supertipo. Por exemplo, não é válida a conversão de um
objeto List<Carro>
para List<Veiculo>.
Consequentemente, o que se segue abaixo não é válido:
List<Carro>
carros = new ArrayList<Carro>();
//a
linha abaixo não é válida – ocorre um erro de compilação
List<Veiculo>
veiculos = carros;
Caso seja necessária uma conversão ampliadora,
podemos utilizar o elemento coringa. Esse é representado por uma interrogação (?) entre as chaves, seguido da palavra extends e a classe a ser declarada.
Aplicando o coringa ao exemplo acima, temos o seguinte
trecho de código:
List<Carro>
carros = new ArrayList<Carro>();
//agora
a linha abaixo é válida, aplicando o coringa
List<?
extends Veiculo> veículos = carros;
Além da utilização da palavra reservada extends junto ao operador coringa, é totalmente
válida a utilização deste junto à palavra reservada super.
Isso indica que a variável de referência pode receber tipos genéricos da classe
declarada, incluindo suas superclasses (chegando até Object).
Além da declaração de tipos, o coringa pode
ser utilizado para outras finalidades dentro do contexto de Generics. Entre
elas, podemos citar a declaração de um método. É válido declarar um método que
receba argumentos genéricos utilizando os coringas. Para a demonstração deste
conceito, vamos modificar o método remover() adicionado à Listagem 3:
public void remover(List<? extends Veiculo>
veiculosParaRemocao) {
veiculos.removeAll(veiculosParaRemocao);
for (Veiculo veiculoRemovido:
veiculosParaRemocao) {
System.out.println(veiculoRemovido.toString());
}
}
Agora podemos usar como argumento um objeto List<Carro> para um método com parâmetro
declarado como List<? extends Veiculo>.
Ou seja, o método irá aceitar tipos parametrizados a partir do tipo declarado
até suas subclasses.
Utilizando o operador coringa não podemos fazer a operação de
inserção em conjuntos, para prevenir a ocorrência de erros em tempo de execução.
Caso utilizássemos este recurso, ao fazer a remoção de um elemento deste
conjunto correríamos o risco de uma ClassCastException
ser lançada
pela tentativa de conversão de um tipo para outro não referente a este.
Conforme citado anteriormente, mesmo não sendo necessário, ainda podemos
utilizar a conversão explícita ao recuperarmos os elementos de uma lista de
objetos.
Criando nossos próprios tipos Genéricos
Outra vantagem de Generics é a possibilidade de criarmos
nossos próprios tipos genéricos. Tomando como exemplo o sistema para gerenciar
estacionamentos, vamos adaptá-lo para o controle de qualquer tipo de objeto a
ser armazenado em um estabelecimento. Para tal finalidade, com base na Listagem 3, na Listagem 5
criamos a classe Estabelecimento para representar um local para armazenarmos qualquer
tipo de objeto.
Assim,
ao invés de declararmos um Veiculo como tipo parametrizado, criamos o elemento
genérico com a notação E. Utilizamos esta notação em Generics para especificar que
esta classe possui um tipo parametrizado genérico. Este, por ser geral, pode
ser substituído por qualquer tipo no momento da instanciação do objeto em
questão.
Existe
uma série de notações para a representação de diversos tipos genéricos
pré-definidos. Na Tabela 1
apresentamos as
principais notações.
|
Tipo
|
Representação Genérica
|
Especificação
|
Exemplo de criação genérica
|
Exemplo de instanciação
|
...