Tipificação Forte com Generics
Tipificação Forte com Generics
Tipificação Forte
Java é uma linguagem de tipificação forte (ou fortemente tipada), o que significa que o tipo de uma variável é verificado em tempo de compilação e não pode ser alterado dinamicamente durante a execução do programa. Isso implica que o compilador Java verifica se as operações realizadas com uma variável são compatíveis com seu tipo declarado.
Por exemplo, você não pode atribuir uma String a uma variável do tipo int.
int numero = 10;
numero = "Texto"; // Erro de compilação: tipos incompatíveisA tipificação forte evita erros comuns, como tentar acessar métodos ou propriedades que não existem para um determinado tipo, reduzindo bugs e facilitando a depuração.
Ao declarar explicitamente os tipos das variáveis, o código se torna mais legível e compreensível, pois fica claro quais dados estão sendo manipulados.
No entanto, a tipificação forte pode exigir mais esforço do desenvolvedor, já que é necessário garantir que os tipos estejam sempre corretos. É aqui que os generics entram em cena, oferecendo flexibilidade sem sacrificar a segurança.
Explicitando Subtipo
Considerando o modelo a seguir:
Pela definição da classe Veiculo, o atributo motor é do tipo Motor. Isso significa que qualquer instância de Veiculo (Carro, Moto ou Caminhao) pode ter um motor de qualquer tipo que herde de Motor, como MotorCombustao ou MotorEletrico.
Caso a classe filha de Veiculo precise de um tipo específico de motor, como MotorCombustao, o código ficaria assim:
public class Carro extends Veiculo {
public Carro(MotorCombustao motor){
super(motor);
}
}Com isso, o construtor de Carro aceita apenas um MotorCombustao, garantindo que o tipo de motor seja consistente com o tipo de veículo.
Apesar da instancia de um Carro sempre ter como instancia de motor um MotorCombustao, o getMotor vai retornar um Motor, sendo necessário fazer um cast para MotorCombustao quando for necessário acessar métodos específicos desse tipo de motor.
Carro carro = new Carro(new MotorCombustao());
Motor motor = carro.getMotor(); // Retorna Motor, mas é um MotorCombustao
MotorCombustao motorCombustao = (MotorCombustao) motor; // Cast
System.out.println("Cilindradas: " + motorCombustao.getCilindradas());Generics
Os generics foram introduzidos no Java 5 para permitir que classes, interfaces e métodos operem com tipos parametrizados. Eles são uma forma de criar código reutilizável e seguro, evitando a necessidade de casts explícitos, e erros de tipo, em tempo de execução.
Antes dos generics, era comum usar Object para criar coleções ou classes que pudessem armazenar qualquer tipo de dado. No entanto, isso exigia casts explícitos e podia levar a erros em tempo de execução.
import java.util.ArrayList;
import java.util.List;
public class ExemploObject {
public static void main(String[] args) {
// Lista sem generics (usa Object)
List lista = new ArrayList();
// Adicionando elementos de tipos diferentes
lista.add("Texto"); // String
lista.add(10); // Integer
lista.add(3.14); // Double
// Recuperando elementos (precisa de cast)
String texto = (String) lista.get(0); // Cast para String
Integer numero = (Integer) lista.get(1); // Cast para Integer
Double decimal = (Double) lista.get(2); // Cast para Double
System.out.println("Texto: " + texto);
System.out.println("Número: " + numero);
System.out.println("Decimal: " + decimal);
// Problema: Erro em tempo de execução se o cast estiver errado
try {
Integer erro = (Integer) lista.get(0); // Cast incorreto (String para Integer)
} catch (ClassCastException e) {
System.out.println("Erro de cast: " + e.getMessage());
}
}
}Com generics, podemos criar coleções ou classes que trabalham com tipos específicos, eliminando a necessidade de casts e garantindo segurança de tipo em tempo de compilação.
import java.util.ArrayList;
import java.util.List;
public class ExemploGenerics {
public static void main(String[] args) {
// Lista com generics (tipo específico: String)
List<String> listaDeStrings = new ArrayList<>();
// Adicionando elementos (apenas Strings são permitidas)
listaDeStrings.add("Texto 1");
listaDeStrings.add("Texto 2");
// listaDeStrings.add(10); // Erro de compilação: tipo incompatível
// Recuperando elementos (sem necessidade de cast)
String texto1 = listaDeStrings.get(0);
String texto2 = listaDeStrings.get(1);
System.out.println("Texto 1: " + texto1);
System.out.println("Texto 2: " + texto2);
// Exemplo com classe genérica
Caixa<Integer> caixaDeInteiros = new Caixa<>();
caixaDeInteiros.setConteudo(42);
int valor = caixaDeInteiros.getConteudo(); // Sem cast
System.out.println("Valor na caixa: " + valor);
}
}
// Classe genérica
class Caixa<T> {
private T conteudo;
public void setConteudo(T conteudo) {
this.conteudo = conteudo;
}
public T getConteudo() {
return conteudo;
}
}Comparativo
| Aspecto | Uso de Objects | Uso de Generics |
|---|---|---|
| Segurança de Tipo | Não há verificação em tempo de compilação. | Verificação em tempo de compilação. |
| Casts | Necessários e propensos a erros. | Não são necessários. |
| Flexibilidade | Aceita qualquer tipo, mas sem segurança. | Aceita tipos específicos com segurança. |
| Erros em Tempo de Execução | Comuns (ex: ClassCastException). | Raros (erros são detectados em compilação). |
Mais exemplos
public class GenericsTest {
public static void main(String args[]) {
// String type test
Test<String> test1 = new Test<String>("Test String.");
test1.showItemDetails();
// Integer type test
Test<Integer> test2 = new Test<Integer>(100);
test2.showItemDetails();
}
}
class Test<T> {
private T item;
public Test(T item) {
this.item = item;
}
public T getItem() {
return item;
}
public void setItem(T item) {
this.item = item;
}
public void showItemDetails() {
System.out.println("Value of the item: " + item);
System.out.println("Type of the item: " + item.getClass().getName());
}
}Exemplo de classe com Generics com dois parâmetros:
public class GenericsTest {
public static void main(String args[]){
//String type test
Test<String, Integer> test =
new Test<String, Integer>("Test String.", 100);
test.showItemDetails();
}
}
class Test<T, U> {
private T itemT;
private U itemU;
public Test(T itemT, U itemU){
this.itemT = itemT;
this.itemU = itemU;
}
public T getItemT() {
return itemT;
}
public void setItemT(T itemT) {
this.itemT = itemT;
}
public U getItemU() {
return itemU;
}
public void setItemU(U itemU) {
this.itemU = itemU;
}
public void showItemDetails(){
System.out.println("Value of the itemT: " + itemT);
System.out.println("Type of the itemT: " + itemT.getClass().getName());
System.out.println("Value of the itemU: " + itemU);
System.out.println("Type of the itemU: " + itemU.getClass().getName());
}
}Restringir o tipo genérico T
O uso de T extends em generics permite restringir o tipo genérico T a uma classe ou interface específica, garantindo que apenas subtipos dessa classe ou interface possam ser usados. Isso é útil para impor limites aos tipos aceitos e acessar métodos ou propriedades específicas da classe ou interface.
Imagine que no exemplo anterior, alguém defina um novo veículo como o exibido abaixo:
public class Pop extends Veiculo<Integer> {
public Pop(String modelo, Integer motor) {
super(modelo, motor);
}
@Override
public void ligar() {
IO.println("Pop " + getModelo() + " com " + getMotor() + " está ligado.");
}
}Faz sentido ter um veículo como Carro passar como tipo T um Interger?
É possível fazer uma restrição para que todos os tipos definidos para o genérico sejam filhos de Motor, por exemplo.
public abstract class Veiculo<T extends Motor> {
private String modelo;
private T motor;
public Veiculo(String modelo, T motor) {
this.modelo = modelo;
this.motor = motor;
}
public String getModelo() {
return modelo;
}
public T getMotor() {
return motor;
}
public abstract void ligar();
}
public class Pop extends Veiculo<Integer> {
public Pop(String modelo, Integer motor) {
super(modelo, motor);
}
@Override
public void ligar() {
IO.println("Pop " + getModelo() + " com " + getMotor() + " está ligado.");
}
}Erro
Com essa restrição, a classe Pop não poderia ser compilada já que Integer não herda de Motor
Herança com Generics
A combinação de herança e generics em Java permite criar hierarquias de classes que são flexíveis e seguras em termos de tipos.
Considere o cenário de um sistema de gerenciamento de veículos, onde devem ser criados diferentes tipos de veículos apresentado no item anterior.
Podemos usar generics para definir na classe base Veiculo um tipo genérico T que representa o tipo de motor do veículo. As subclasses podem então especificar o tipo de motor que usam.
As subclasses de Veiculo que especificam o tipo de motor.
Carro: Usa umMotorCombustao.Moto: Usa umMotorCombustao.Caminhao: Usa umMotorEletrico.
public abstract class Veiculo<T extends Motor> {
private String modelo;
private T motor;
public Veiculo(String modelo, T motor) {
this.modelo = modelo;
this.motor = motor;
}
public String getModelo() {
return modelo;
}
public T getMotor() {
return motor;
}
public abstract void ligar();
}A classe Veiculo é uma classe que aceita um tipo T genérico para o motor. Ela define comportamentos comuns para todos os veículos.
public class MotorCombustao extends Motor {
private int cilindradas;
public MotorCombustao(int cilindradas) {
this.cilindradas = cilindradas;
}
public int getCilindradas() {
return cilindradas;
}
@Override
public String toString() {
return "Motor Combustão (" + cilindradas + "cc)";
}
}public class MotorEletrico extends Motor {
private int potenciaKW;
public int getPotenciaKW() {
return potenciaKW;
}
public MotorEletrico(int potenciaKW) {
this.potenciaKW = potenciaKW;
}
@Override
public String toString() {
return "Motor Elétrico (" + potenciaKW + "kW)";
}
}
public class Carro extends Veiculo<MotorCombustao> {
public Carro(String modelo, MotorCombustao motor) {
super(modelo, motor);
}
@Override
public void ligar() {
IO.println("Carro " + getModelo() + " com " + getMotor() + " está ligado com "+getMotor().getCilindradas()+" Cilindradas" );
}
}
public class Moto extends Veiculo<MotorCombustao> {
public Moto(String modelo, MotorCombustao motor) {
super(modelo, motor);
}
@Override
public void ligar() {
IO.println("Moto " + getModelo() + " com " + getMotor() + " está ligada com "+getMotor().getCilindradas()+" Cilindradas" );
}
}
public class Caminhao extends Veiculo<MotorEletrico> {
public Caminhao(String modelo, MotorEletrico motor) {
super(modelo, motor);
}
@Override
public void ligar() {
IO.println("Caminhão Elétrico" + getModelo() + " com " + getMotor() + " está ligado com "+getMotor().getPotenciaKW()+" de Potencia" );
}
}Testando a Hierarquia
public class TestaVeiculos {
public static void main(String[] args) {
// Criando motores
MotorCombustao motorCarro = new MotorCombustao(2000);
MotorCombustao motorMoto = new MotorCombustao(600);
MotorEletrico motorCaminhao = new MotorEletrico(300);
// Criando veículos
Carro carro = new Carro("Sedan", motorCarro);
Moto moto = new Moto("Esportiva", motorMoto);
Caminhao caminhao = new Caminhao("Carga Pesada", motorCaminhao);
// Ligando os veículos
carro.ligar();
moto.ligar();
caminhao.ligar();
}
}Carro Sedan com Motor Combustão (2000cc) está ligado.
Moto Esportiva com Motor Combustão (600cc) está ligada.
Caminhão Elétrico Carga Pesada com Motor Elétrico (300kW) está ligado.A grande vantagem dessa abordagem é que ao chamar o método getMotor, o tipo retornado é específico para cada veículo, eliminando a necessidade de casts e aumentando a segurança do tipo.
public class TestaVeiculos {
public static void main(String[] args) {
// Criando motores
MotorCombustao motorCarro = new MotorCombustao(2000);
MotorCombustao motorMoto = new MotorCombustao(600);
MotorEletrico motorCaminhao = new MotorEletrico(300);
// Criando veículos
Carro carro = new Carro("Sedan", motorCarro);
Moto moto = new Moto("Esportiva", motorMoto);
Caminhao caminhao = new Caminhao("Carga Pesada", motorCaminhao);
//sem necessidade de cast para MotorCombustao
IO.println(carro.getMotor().getCilindradas());
IO.println(moto.getMotor().getCilindradas());
//sem necessidade de cast para MotorEletrico
IO.println(caminhao.getMotor().getPotenciaKW());
// Ligando os veículos
carro.ligar();
moto.ligar();
caminhao.ligar();
}
}Outro Exemplo
public interface FormaGeometrica {
double calcularArea();
}public class Circulo implements FormaGeometrica {
private double raio;
public Circulo(double raio) {
this.raio = raio;
}
@Override
public double calcularArea() {
return Math.PI * Math.pow(raio, 2);
}
}public class Retangulo implements FormaGeometrica {
private double base;
private double altura;
public Retangulo(double base, double altura) {
this.base = base;
this.altura = altura;
}
@Override
public double calcularArea() {
return base * altura;
}
}import java.util.ArrayList;
import java.util.List;
public class CalculadoraArea<T extends FormaGeometrica> {
private List<T> formas;
public CalculadoraArea() {
this.formas = new ArrayList<>();
}
public void adicionarForma(T forma) {
formas.add(forma);
}
public double calcular() {
return formas.stream().mapToDouble(FormaGeometrica::calcularArea).sum();
}
}public class TestaCalculadoraArea {
public static void main(String[] args) {
// Criando formas geométricas
Circulo circulo = new Circulo(5.0);
Retangulo retangulo = new Retangulo(4.0, 6.0);
// Calculando áreas
CalculadoraArea<Circulo> calculadoraCirculo = new CalculadoraArea<>(circulo);
CalculadoraArea<Retangulo> calculadoraRetangulo = new CalculadoraArea<>(retangulo);
System.out.println("Área do círculo: " + calculadoraCirculo.calcular());
System.out.println("Área do retângulo: " + calculadoraRetangulo.calcular());
}
}Área do círculo: 78.53981633974483
Área do retângulo: 24.0Exemplo de Erro de Tipo
Se tentarmos usar um tipo que não implementa FormaGeometrica, o compilador gerará um erro:
// Erro de compilação: String não implementa FormaGeometrica
CalculadoraArea<String> calculadoraInvalida = new CalculadoraArea<>("Texto");Referências
Jai, Generics class Java, W3schools, https://www.w3schools.blog/generics-class-java ↩︎