Mecanismo de Herança
A herança constitui um pilar estrutural que permite a especialização de classes. Ao derivar uma nova estrutura a partir de uma existente, reutilizam-se comportamentos e atributos previamente implementados, possibilitando a adição de funcionalidades específicas sem reescrever o código base. É importante observar que a herança é absoluta: não é possível selecionar apenas partes da classe progenitora.
Três aspectos fundamentais regem essa relação:
- Subclasses herdam automaticamente todos os membros visíveis (não privados) da superclasse.
- A classe derivada pode estender o modelo original, introduzindo novos campos e métodos.
- É permitido redefinir o comportamento de métodos existentes, adaptando-os ao novo contexto.
Inicialização e Encadeamento de Construtores
Construtores não são herdados, mas sua execução é obrigatória durante a instanciação. A JVM garente que o inicializador da classe base seja invocado antes do da classe derivada. Caso a superclasse possua um construtor padrão (sem parâmetros), a chamada é inserida implicitamente pelo compilador. Quando apenas construtores paramettrizados existem na classe pai, o desenvolvedor deve explicitamente utilizar super() na primeira linha do construtor filho.
class BaseTecnologica {
protected String modulo;
BaseTecnologica() {
System.out.println("Base inicializada (padrão).");
}
BaseTecnologica(String nomeModulo) {
this.modulo = nomeModulo;
System.out.println("Base configurada com: " + modulo);
}
}
class ComponenteAvancado extends BaseTecnologica {
private double capacidade;
ComponenteAvancado(double cap) {
// Chamada explícita obrigatória se a base não tiver construtor padrão acessível
super("Modulo-X");
this.capacidade = cap;
System.out.println("Componente especializado configurado.");
}
public static void main(String[] args) {
new ComponenteAvancado(85.5);
}
}
Se a classe base não declarar um construtor sem argumentos e a filha não chamar super() explicitamente, ocorrerá um erro de compilação. A ordem de execução é sempre ascendente: da raiz para a folha.
Visibilidade e Acoplamento
O modificador protected expõe membros exclusivamente ao pacote atual e às subclasses, facilitando a extensão. Contudo, a prática recomendada mantém os atributos de instância como private, expondo apenas métodos protegidos para acesso controlado, preservando a independência da implementação base.
A herança impõe desvantagens arquiteturais relevantes: a classe base torna-se um ponto de falha (alterações na raiz impactam toda a hierarquia), a exposição de detalhes internos fragiliza a ocultação de dados e cria dependências rígidas entre componentes.
Prática do Encapsulamento
O encapsulamento isola o estado interno dos objetos, restringindo o acesso direto aos atributos e exigindo a interação por meio de contratos públicos (métodos). Esse mecanismo centraliza o controle sobre como os dados são lidos ou alterados, unindo estrutura e comportamento em uma única unidade coesa.
Os principais benefícios incluem a capacidade de refatorar a lógica interna sem impactar o código cliente e a validação rigorosa de entradas. Ao invés de permitir atribuição direta, os métodos setters atuam como filtros.
class SensorAmbiente {
private int temperaturaAtual;
private String estadoSistema;
public int obterLeitura() {
return temperaturaAtual;
}
public void atualizarValor(int novoValor) {
if (novoValor < -50 || novoValor > 60) {
System.err.println("Alerta: leitura fora da faixa operacional segura.");
return;
}
this.temperaturaAtual = novoValor;
}
public String obterStatusVisual() {
if (this.temperaturaAtual > 40) {
return "CRÍTICO";
} else if (this.temperaturaAtual > 20) {
return "ESTÁVEL";
}
return "FRIO";
}
}
Neste modelo, a validação no atualizarValor() previne estados inválidos, enquento o método obterStatusVisual() transforma um dado bruto em uma representação semanticamente útil para interfaces de usuário, sem expor a lógica de conversão.
Dinâmica do Polimorfismo
O polimorfismo permite que uma única referência a um tipo genérico execute comportamentos distintos dependendo da instância concreta atribuída a ela em tempo de execução. O princípio fundamental é tratar objetos derivados como se fossem da sua classe base, delegando a execução ao método mais especializado disponível.
Tipos e Requisitos Estruturais
Na linguagem Java, manifesta-se de duas formas: estática (sobrecarga de métodos, resolvida em tempo de compilação) e dinâmica (sobrescrita, resolvida durante a execução). Para o polimorfismo dinâmico funcionar, três condições são indispensáveis: existência de hierarquia de herança, implementação do mecanismo de sobrescrita e declaração de uma variável do tipo base referenciando um objeto do tipo derivado.
Resolução de Membros e Verificação de Tipos
O comportamento do compilador e da máquina virtual difere conforme o tipo de membro acessado:
- Campos de dados: o acesso é definido pela classe declarada da variável (tempo de compilação).
- Métodos de instância: a execução é determinada pelo tipo real do objeto em memória (tempo de execução), desde que ocorra sobrescrita.
Para converter referências entre níveis da hierarquia, utiliza-se o upcasting (implícito e seguro) e o downcasting (explícito e sujeito a validação). O operador instanceof é essencial para evitar exceções em tempo de execução antes de realizar conversões descendentes.
abstract class DispositivoEntrada {
public void calibrar() {
System.out.println("Calibração genérica executada.");
}
}
class MouseOptico extends DispositivoEntrada {
@Override
public void calibrar() {
System.out.println("Ajustando sensibilidade do DPI.");
}
public void configurarMacro() {
System.out.println("Macro programada.");
}
}
class TecladoMecanico extends DispositivoEntrada {
@Override
public void calibrar() {
System.out.println("Ajustando tempo de debounce.");
}
public void ativarBacklight() {
System.out.println("Iluminação RGB ativada.");
}
}
public class GerenciadorPerifericos {
public static void main(String[] args) {
// Upcasting automático: variável do tipo base aponta para instância derivada
DispositivoEntrada perif1 = new MouseOptico();
perif1.calibrar(); // Executa versão sobrescrita
// Necessidade de funcionalidade específica exige downcasting
if (perif1 instanceof TecladoMecanico) {
TecladoMecanico teclado = (TecladoMecanico) perif1;
teclado.ativarBacklight();
} else if (perif1 instanceof MouseOptico) {
MouseOptico mouse = (MouseOptico) perif1;
mouse.configurarMacro(); // Executa método exclusivo
}
}
}
A referência genérica perif1 invoca o método sobrescrito corretamente. Para acessar funcionalidades exclusivas da implementação concreta, a verificação de tipo seguida de conversão explícita restaura a visibilidade dos métodos especializados, mantendo a segurança de tipagem.