Nessa aula, damos sequência a ideia de organizar nossos conceitos em uma hierarquia de classes, falando das consequências disso em termos de métodos e tipagem no Java.
Ao final da aula, saberemos como nos referirmos a objetos por tipos diferentes na hierarquia de classes.
Quando herdamos de uma classe estamos também criando uma relação semântica de hiponímia e hiperonímia, onde a subclasse é um hipônimo da superclasse (relação is-a).
Os tipos da linguagem Java seguem essa mesma relação: um objeto de um subtipo é também um objeto de seu supertipo. Ou seja, uma instância de Funcionario também é uma instância de Pessoa, e uma instância de Carro é também uma instância de VeiculoComRodas.
Mas o que isso significa em termos do código?
A primeira consequência desse fato é que podemos atribuir uma referência de um objeto a uma variável de seu supertipo.
O funcionário que foi criado no exemplo abaixo é referenciado por uma variável do tipo Pessoa, mas não deixa de ser do tipo Funcionario.
Chamamos de polimorfismo a capacidade de nos referir a um mesmo objeto via mais de um tipo.
Pessoa pessoa = new Funcionario("123456789-00", "Fulano da Silva", 321);
Isso quer dizer que se um método for chamado através dessa variável, e ele existir nas classes Pessoa e Funcionario, será chamado o método da classe Funcionario caso haja um override, e caso não, será chamado o método da classe Pessoa.
Por exemplo: para garantir que a biblioteca não emprega menores de idade, sobrescrevemos o setter da data de nascimento de Pessoa, para verificar a idade antes de atribuir. Podemos fazer tal verificação também no construtor.
Se o método setDataNascimento for chamado usando a variável pessoa no exemplo abaixo, a atribuição não será feita pois o método da classe Funcionário será chamado e a verificação de idade resultará na impressão da mensagem de erro programada.
Uma chamada a um método sobrescrito em uma subclasse através de uma referência a sua superclasse é conhecida como chamada de método virtual.
O compilador valida a chamada usando a declaração do método na superclasse, mas a JVM só fará a chamada ao método correto durante a execução. Daí o nome "virtual", pois a chamada não está definida de fato no bytecode.
Pessoa pessoa = new Funcionario("123456789-00", "Fulano da Silva", 321);
private class Funcionario extends Pessoa {
...
@Override
public void setDataNascimento(LocalDateTime dataNascimento) {
if (Pessoa.calculaIdade(dataNascimento) >= 18) {
this.dataNascimento = dataNascimento;
}
else {
System.err.println("Erro: Funcionários devem ter mais de 18 anos de idade.");
}
}
}
pessoa.setDataNascimento(new LocalDateTime.now().minusDays(1));
Erro: Funcionários devem ter mais de 18 anos de idade.
pessoa.setDataNascimento(new LocalDateTime.now().minusDays(1));
Esses recursos são tipicamente utilizados no intento de generalizar os argumentos de métodos, aceitando múltiplos tipos complementares e deixando a decisão de qual método chamar em cada ponto para a JVM.
Não é difícil imaginar situações em que precisaremos verificar ou manipular pessoas no sistema da biblioteca independente de serem funcionários ou usuários. Fazemos isso sabendo que cada objeto possui sua própria maneira de manter seu estado interno, e essa maneira depende do tipo.
O polimorfismo e as chamadas de método virtual adicionam novos recursos ao nosso arsenal de encapsulamento, permitindo abstrair até mesmo qual método será chamado para uma dada referência.
Entretanto, o polimorfismo não permite o intercâmbio de tipos irmãos ou de tipos não complementares.
E se eu quisesse declarar que a minha classe permite certas ações, mas não quisesse implementá-las de imediato?
Perguntas:
Exercício:
Até a próxima aula!