Herança e sobrescrita de métodos - Programação Orientada a Objetos

Prof. Marcelo Cohen

08/2016

Sumário

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:
figure vooescalas-semheranca.png
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:
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:
figure vooescalas-heranca.png

Exercício 1