Herança e sobrescrita de métodos - Programação Orientada a Objetos
Prof. Marcelo Cohen
08/2016
1 Motivação
Vamos supor que desejamos criar um mecanismo para vôos com uma escala. Uma forma de fazer isso é criando uma classe chamada VooEscalas, que poderia ser implementada da seguinte forma:
public class VooEscalas
{
public enum Status { CONFIRMADO, ATRASADO, CANCELADO };
private LocalDateTime datahora;
private Duration duracao;
private Rota rota;
private Rota rotaFinal;
private Status status;
// Construtor
public VooEscalas(Rota rota, Rota rotaFinal, LocalDateTime dh, Duration dur) {
...
}
// Gets, etc
...
@Override
public String toString() {
return status + " " + datahora + "("+duracao+"): " + rota + " -> " + rotaFinal;
}
}
O problema dessa abordagem é que todos os métodos em comum com Voo precisam ser novamente implementados, e com isso, há uma considerável repetição de código:
Além disso, qualquer alteração num atributo (ex: trocar a duração para um inteiro) implica em alterar nas duas classes, o que também não é prático. Então, qual é a solução?
2 Herança
2.1 Princípios de modelagem
A solução é um mecanismo de reutilização de código denominado herança. Para realizar uma modelagem com herança, é preciso identificar elementos em comum entre as classes:
-
Atributos em comum: rota, datahora, duracao
-
Métodos em comum: getRota, getDataHora, getDuracao
A partir dessa identificação inicial, cria-se uma classe apenas com esses elementos - essa classe será a base para a construção das outras. Neste exemplo, podemos utilizar a classe Voo que já temos, uma vez que representa genericamente um vôo:
A partir da classe Voo, identificamos todos os elementos que faltam para representar vôos com uma escala:
O próximo passo então é modificar a classe VooEscalas, incluindo a palavra reservada extends, que indica herança. Além disso, é preciso retirar todos os atributos e métodos já incluídos na classe Voo. A classe original é denominada classe base, ou superclasse. A classe que herda é denominada classe derivada, ou subclasse. A classe VooEscalas ficaria da seguinte forma:
public class VooEscalas extends Voo
{
private Rota rotaFinal;
// Construtor
public VooEscalas(Rota rota, Rota rotaFinal, LocalDateTime datahora, Duration duracao) {
this.rota = rota; // ERRO!
this.datahora = datahora // ERRO!
this.duracao = duracao; // ERRO!
this.rotaFinal = rotaFinal;
}
public Rota getRotaFinal() { return rotaFinal; }
...
@Override
public String toString() {
return status + " " + datahora + "("+duracao+"): " + rota + " -> " + rotaFinal; // ERRO!
}
}
2.2 Reuso e sobrescrita
Restam dois problemas: o construtor e o método toString. Da forma atual, eles tentam utilizar atributos que não estão mais presentes (rota, datahorao e duracao). No caso do construtor, como os atributos não pertencem à classe VooEscalas eles devem ser repassados ao construtor de Voo. Isso é denominado reuso, e é feito através da chamada super(...):
// Construtor
public VooEscalas(Rota rota, Rota rotaFinal, LocalDateTime datahora, Duration duracao) {
super(rota, datahora, duracao); // chama o construtor de Voo
this.rotaFinal = rotaFinal;
}
}
No caso do método toString, é importante observar que ele está sendo sobrescrito (override), pois já existe na classe Voo. Por esse motivo existe a anotação @Override antes do método: ela indica para o compilador que sabemos que estamos sobrescrevendo um método herdado. A anotação não é obrigatória (até a versão atual - Java 8), porém é recomendada. De qualquer forma, uma alternativa para resolver o problema é utilizar os métodos get herdados, que são públicos na classe Voo, e portanto, podem ser utilizados pela classe VooEscalas:
@Override
public String toString() {
rreturn getStatus() + " " + getDataHora() + "("+getDuracao()+"): " + getRota() + " -> " + rotaFinal;
}
Porém, se um dos objetivos da herança é justamente favorecer o reaproveitamento de código, então deve haver uma forma de fazer isso, ou seja, reutilizar o que já está pronto na classe Voo. Isso é possível utilizando novamente a sintaxe super (para reuso), mas dessa vez chamando explicitamente um método da superclasse:
@Override
public String toString() {
return super.toString() + " -> " + rotaFinal;
}
Assim como as interfaces, no diagrama UML a herança é representada por uma seta (desta vez sólida), partindo da classe derivada para a superclasse. Dessa forma, um diagrama com as duas classes ficaria assim:
Exercício 1
-
Construa e implemente uma classe que seja capaz de representar um vôo com um número arbitrário de escalas (VooVariasEscalas, por exemplo). A classe deve ser derivada (i.e. subclasse) de Voo ou VooEscalas? Não esqueça de explorar reuso no método toString.
-
Reflita sobre a modelagem em si: o que foi feito parece correto? Será que existe outra forma de modelar essas classes?
-
Implemente um programa principal (ou altere o código da AppTeste) para criar objetos Voo, VooEscalas e VooVariasEscalas, testando os seus métodos.