Temas abordados:
- ✔️ Parte 1 - Construindo os pilares do paradigma (Abstração e Encapsulamento).
- ➡️ Parte 2 - Construindo os pilares do paradigma (Polimorfismo e Herança).
- Parte 3 - Relações entre objetos.
- Parte 4 - Fixando os conceitos da POO com mais prática.
- Parte 5 - Os princípios S.O.L.I.D
- Parte 6 - Aplicando Padrões de projeto (Design Pattern).
- Parte 7 - OO de forma inteligente com mais prática.
- Parte 8 - OO para a vida: Os conceitos de uma perspectiva diferente.
Por que herança e polimorfismo estão juntos?
Decidi por herança e polimorfismo juntos porque eles são complemento um do outro. Ambos têm o mesmo propósito, reusar código.
Mas não pense que somente esses dois têm relação, a herança vai tornar a abstração e o encapsulamento mais complexos, então se você ainda não viu o post anterior, sugiro que leia antes de continuar.
Herança
O conceito da herança toma muito da biologia: A herança permite que uma classe possa recusar ou modificar definições de outra classe. As definições que digo são os membros da classe (atributos e métodos), mas não só isso, porque você também pode sobrescrever alguns deles, isso está mais para o conceito de polimorfismo, então primeiro, vamos explorar a herança.
Superclasses e subclasses
Como vimos, herança reusa código de outra classe, então precisamos ter no mínimo duas classes, vamos criar nossas classes também seguindo um pouco da biologia:
public class Animal {
private String nome;
public Animal(String nome) {
this.nome = nome;
}
public String getNome() {
return nome;
}
}
Como vimos anteriormente, podemos criar um objeto a partir dela dá usando o new
:
Animal animal = new Animal("Cachorro");
Dessa forma, podemos ter quantos animais quiser, agora vamos criar o método fazerBarulho
:
public void fazerBarulho() {
System.out.println("???");
}
Mas como vamos saber de qual animal estamos lidando?
Podemos ler do atributo
nome
e criar umif
/else
ouswitch
para cada possibilidade.
Isso seria muito difícil de cobrir cada caso, além de muito difícil de manter, além disso, isso não vai nos permitir de ter ações específicas para cada tipo de animal, por exemplo, se for uma ave, ter um método voar (sei que nem toda ave voa, mas vamos considerar que sim para simplificar nosso exemplo).
Perceba que ter um animal é muito abstrato, mais do que o exemplo do Livro
na parte anterior, precisamos de algo mais específico, vamos usar a classe Animal
para criar uma outra classe que seja mais específica que ela, podemos começar com a ave, então:
public class Ave extends Animal {
public Ave(String nome) {
super(nome);
}
}
Para herdar de uma outra classe usamos a palavra-chave extends
, seguido do nome da classe que queremos herdar, neste caso estamos criando a classe Ave
que herda de Animal
.
Perceba que Ave já tem um construtor (método com o mesmo nome da classe e sem retorno declarado), nele recebemos uma String nome
e logo passamos para super
, mas o que é este super
?
Da mesma forma que o this
faz referência a nossa classe atual (Ave
), o super
faz referência a nossa superclasse, a classe de quem somos subclasse. Agora:
Ave
é subclasse deAnimal
.Animal
é superclasse deAve
.
Quando uma classe herda de outra, o que acontece com a subclasse:
- Declarar novos atributos que não estão na superclasse;
- Criar novos métodos que não fazem parte da superclasse;
- Membros não privados da superclasse podem ser usados na subclasse;
- A subclasse pode chamar o construtor da superclasse.
A nossa classe Animal
tem um construtor que possui parâmetros, lembre-se que só podemos criar um objeto pelo construtor, mesmo que a nossa classe Animal
esteja “escondida” pela classe Ave
ainda devemos chamar o seu construtor, até porque tudo que tem Animal
faz parte de Ave
, para chamar o construtor da nossa superclasse simplesmente usamos a palavra-chave super
e passamos os parâmetros nos parênteses, semelhante a instanciar, como só temos o parâmetro nome
em Animal
então é só isso que devemos passar.
Vale ressaltar que somente membros com modificadores de acesso mais permissivos que private
podem ser acessados nas subclasses, ou seja, em Animal
, não podemos acessar diretamente o atributo nome
, por hora, apenas getNome
e fazerBarulho
, o que não é um problema para nós agora. Antes de continuar com a resolução do nosso problema do método fazerBarulho
, vamos resolver um pequeno detalhe.
Classes abstratas
Note que animal ainda pode ser instanciado, mas como havia dito anteriormente, esta classe é bastante abstrata para ser usada sozinha, pois não sabemos qual animal especificamente estamos lidando, então podemos fazer dela uma classe abstrata da seguinte forma:
public abstract class Animal { /*...*/ }
Tornar a classe abstrata significa que ela não pode mais ser instanciada, somente herdada, isso vai nos garantir que só existirão subclasses de Animal
e nunca somente Animal
.
Métodos abstratos
Para resolver nosso problema com o método fazerBarulho
, podemos torná-lo abstrato também, da seguinte forma:
public abstract void fazerBarulho();
Isso significa que o método DEVE ser implementada nas subclasses que virão, pois perceba que fazerBarulho
não tem mais implementação na classe Animal
.
Isso vai nos permitir criar uma implementação específica para os animais, assim cada um deles pode fazer um barulho diferente.
Até agora, nossa classe animal está assim:
public abstract class Animal {
private String nome;
public Animal(String nome) {
this.nome = nome;
}
public String getNome() {
return nome;
}
public abstract void fazerBarulho();
}
Agora vamos criar uma implementação para o método fazerBarulho
na classe Ave
:
@Override
public void fazerBarulho() {
System.out.println("Piu?");
}
💡 Note a anotação
@Override
no topo do método, isso indica ao compilador que o membro em questão deve ser sobrescrever o membro declarado na superclasse.
Mas que ave exatamente estamos nos referindo, bem, a classe Ave
ainda parece ser bastante abstrata, pois não sabemos ainda exatamente com qual ave estamos lidando.
O que podemos fazer é criar um if
/else
verificando o nome da ave, mas como disse, esta forma não é simples de manter e muito menos escalável, então agora vamos criar uma implementação concreta da nossa classe Ave
.
Mas antes, vamos marcar a classe Ave como abstrato e colocar algumas propriedades que somente aves possuem para diferenciar dos outros animais, além disso, vamos remover a implementação de fazerBarulho
da classe Ave
, então nossa classe ficará assim no final:
public abstract class Ave extends Animal {
public boolean podeVoar;
public Ave(String nome, boolean podeVoar) {
super(nome);
this.podeVoar = podeVoar;
}
}
Mas porque remove o método
fazerBarulho
se você disse que ele DEVE ser implementado?
A classe Ave
agora é uma classe abstrata, o que significa que ela foi criada para ser herdada e não pode ser instanciada, os membros abstratos são opcionalmente implementados em classes abstratas, mas devem ser implementadas em classes concretas (aquelas que serão instanciadas).
Como nossa classe Ave
agora é abstrata, então não precisamos sobrescrever os membros abstratos da nossa classe pai agora.
Implantação concreta
Seguindo, vamos agora criar a nossa classe concreta que herda de Ave
:
Começando pelo construtor:
public Galinha() {
super("Galinha", false);
}
Note que o super faz referência a classe Ave
, então no construtor devemos chamar o super
no nosso construtor tendo isso em mente.
Veja também que, Galinha
não recebe parâmetros, isso porque agora, podemos inferir todos os parâmetros que a nossa superclasse recebe, e como não criei nenhuma nova propriedade para Galinha
então não há nada que queiramos receber.
Vale ressaltar que nem todo case será desta forma, isso aconteceu apenas por conta da forma que abstraímos as nossas classes.
Agora para a implementação do método fazerBarulho
:
@Override
public void fazerBarulho() {
System.out.println("Co Co");
}
Agora que temos finalmente uma implementação concreta da nossa classe e dos nossos membros.
A classe Galinha ficou da seguinte forma:
public class Galinha extends Ave {
public Galinha() {
super("Galinha", false);
}
@Override
public void fazerBarulho() {
System.out.println("Co Co");
}
}
Vamos instanciar nossa classe e chamar o método fazerBarulho
:
Galinha ave = new Galinha();
animal.fazerBarulho();
Saida:
Co Co
Classes e tipos
Galinha
herdade deAve
que por sua vez herda deAnimal
, isso faz de uma instancia deGalinha
uma instancia deAnimal
também?
Sim, um objeto pode ter múltiplos tipos dependendo de sua hierarquia, isso significa que que um objeto do tipo Galinha
também é do tipo Ave
e Animal
. Em um exemplo:
Podemos fazer:
Animal animal = new Galinha();
pois Galinha
herda de Animal
. Mas temos uma ressalva:
Apesar de nossa variável guardar uma instancia de Galinha
, o tipo dela é Animal
, o que significa que só teremos acesso aos membros do tipo Animal
.
Isso significa que se quiséssemos acessar o podeVoar
, não vamos conseguir, pois esta é uma propriedade de Galinha
.
Isso é útil quando queremos acessar apenas os membros de um tipo mais abstrato.
Vamos fazer um exemplo com o tipo Ave
.
Primeiro vou criar uma nova classe que implementa Ave
, seguindo a mesma forma de Galinha
:
public class Gaviao extends Ave {
public Gaviao() {
super("Gavião", true);
}
@Override
public void fazerBarulho() {
System.out.println("AAA AAA");
}
}
Agora vou criar um método na classe Main
:
private void consegueVoar(Ave ave) {
String podeVoarMessage = String.format("%s pode voar", ave.getNome());
String nPodeVoarMessage = String.format("%s não pode voar", ave.getNome());
if(ave.podeVoar) {
System.out.println(podeVoarMessage);
return;
}
System.out.println(nPodeVoarMessage);
}
Veja que este método recebe uma variável do tipo Ave
, isso significa que podemos passar qualquer classe/tipo que herde de Ave
, pois vamos usar apenas os membros deste tipo.
public static void main(String[] args) {
Ave galinha = new Galinha();
Ave gaviao = new Gaviao();
consegueVoar(galinha);
consegueVoar(gaviao);
}
Saida:
Galinha não pode voar
Gavião pode voar
E o polimorfismo?
O polimorfismo na OO, é a forma que uma subclasse pode definir seu próprio comportamento e compartilhar alguma funcionalidade da sua superclasse.
Para você que ainda não percebeu, foi exatamente o que fizemos nos exemplos acima, onde fica ainda mais claro o que fizemos no método fazerBarulho
, onde a classe Animal
define apenas a assinatura e suas subclasses devem criar sua própria implementação deste método.
Veja que herança e polimorfismo estão tão relacionados que acabamos implementando os dois conceitos sem perceber, de forma natural, um acontece por causa do outro.
Interfaces
Em alguns casos, não necessitamos de uma nova classe para definir um comportamento para nossa classe, as interfaces são uma forma de definir um “contrato” para as classes que implementam ela, uma interface dita como usamos uma classe especificamente, se assemelha bastante com o que fazemos com classes abstratas, inclusive, interfaces também não podem ser instanciadas, mas ao contrário delas, as interfaces só devem conter assinaturas de métodos como membros, variáveis e campos não são permitidos. Vamos começar outro exemplo.
Situação
Estou criando um sistema que pode ser integrado com outros, para tornar a tarefa de integração fácil para quem irá usar, vamos criar uma interface, como dito, isso irá firmar uma espécie de contrato para com meu sistema e quem irá implementar fazer de forma correta.
Então vamos criar nossa interface:
public interface SistemaUniversal {
void verTodosUsuario();
void criarUsuario(String nome);
void deletarUsuario(String nome);
void atualizarUsuario(String nomeAtual, String novoNome);
}
A interface SistemaUniversal
define apenas as assinaturas dos métodos que será usado tanto para quem vai implementar a interface quanto para quem vai usar.
Uma interface contém apenas assinaturas dos métodos, mas nunca sua implementação, pois isso será definido pela classe que irá implementá-lo, coisa que iremos fazer agora:
Primeiro devemos definir que esta classe irá implementar a interface com a palavra-chave implements
:
public class Sistema1 implements SistemaUniversal { /*...*/ }
Agora, DEVEMOS implementar todos os métodos que definimos na interface:
public class Sistema1 implements SistemaUniversal {
private final ArrayList<String> usuarios;
public Sistema1() {
this.usuarios = new ArrayList<String>();
}
@Override
public void verTodosUsuario() {
for (final String usuario : usuarios) {
System.out.println(usuario);
}
}
@Override
public void criarUsuario(String nome) {
usuarios.add(nome);
}
@Override
public void deletarUsuario(String nome) {
usuarios.remove(nome);
}
@Override
public void atualizarUsuario(String nomeAtual, String novoNome) {
final int toUpdateIndex = usuarios.indexOf(nomeAtual);
usuarios.remove(nomeAtual);
usuarios.add(toUpdateIndex, novoNome);
}
}
verTodosUsuario
: Exibe cada elemento da lista de usuários.criarUsuario
: Adiciona o valor que recebemos pelo parâmetro na lista de usuários.deletarUsuario
: Remove o usuário da lista pelo valor.atualizarUsuario
: Recebe um parâmetro contendo o nome atual do usuário que terá seu nome atualizado e o novo nome, pega o index de onde este nome de usuário se encontra, remove ele, e insere o novo nome no mesmo local que o antigo.
Agora temo um sistema que implementa a interface SistemaUniversal
.
Assim como uma classe, uma interface também pode ser um tipo, então da mesma forma que fizemos com Ave
, vamos fazer com SistemaUniversal
:
public static void main(String[] args) {
SistemaUniversal sistemaUniversal = new Sistema1();
sistemaUniversal.criarUsuario("Roger");
sistemaUniversal.verTodosUsuario();
}
Saida:
Roger
Da mesma forma do exemplo anterior, desta forma temos acesso somente ao que declaramos em SistemaUniversal
, mas dependendo do real tipo que temos na variável (tipo durante o runtime) o comportamento pode mudar.
Conclusão
Os dois assuntos, herança e polimorfismo estão tão relacionados que até mesmo o propósito dos dois são os mesmos, reusar código. Antes de um exemplo mais elaborado, juntando todos estes conceitos básicos, devemos dar uma rápida olhada sobre relacionamento entre objetos, que abordaremos no próximo post.
Como sempre, o código que vimos aqui está disponível no repositório abaixo:
Peace ✌️