Aula 2 - Herança

Suponha que você está fazendo um sistema para gerenciar uma locadora de vídeo que aluga e vende DVDs. Naturalmente, você precisa de pelo menos duas classes para cuidar dos seus produtos:

class DVDdeVender {
   private float preço;
   public void vender();
   public void devolver();
   public void recibo();
}

class DVDdeAlugar {
   private float preço;
   private Date dataDevolução;
   public void alugar();
   public void devolver();
   public void recibo();
}

No entanto, você nota que as duas classes são muito semelhantes e gostaria de não ter de repetir esforços na sua criação e uso. O mecanismo que permite isso é a herança.

Herança é um relacionamento entre classes que é uma das características principais da orientação a objetos. A classe herdada é a classe pai, ou superclasse, e a classe herdadora é a classe filha, ou subclasse.

A classe filha herda todos os membros (dados e métodos) da classe pai. No entanto, a classe filha só tem acesso direto a membros declarados como public ou protected.

Interfaces e implementações:

Há duas motivações principais para o uso da herança:

  • Herança para construção
  • Herança para substituição

    Herança para construção

  • Usa-se herança para construção quando o objetivo é reutilizar uma classe existente para criar uma semelhante.
  • Um ou mais métodos são sobrepostos para se mudar a funcionalidade da classe.
  • Objetivo é reaproveitamento de código.
  • Em geral, o uso de herança somente para construção é fruto de de um projeto ruim; deve-se buscar soluções que usem também herança para substituição.

    Exemplo:

    class DVDdeVender {
       private float preço;
       public void adquirir();
       public void devolver();
       public void recibo();
    }
    
    class DVDdeAlugar extends DVDdeVender {
       private Date dataDevolução;
       public void adquirir();
       public void devolver();
    }
    

    Herança para substituição

    No exemplo acima, o uso de herança reduziu a quantidade de código necessária da construção da classe. A herança também nos permite reduzir o código necessário para o uso da classe. Por exemplo, suponha que tenhamos a seguinte classe para armazenar nosso estoque de DVDs:

    public class ColeçãoDeDVDs {
       private DVDdeAlugar[] dvdsDeAlugar;
       private DVDdeVender[] dvdsDeVender;
       private int numeroDVDsAlugar;
       private int numeroDVDsVender;
       
       public void acrescentarDVDdeVender(DVDdeVender d) {
       		  dvdsDeVender[numeroDVDsVender] = d;
    		  numeroDVDsVender++; 
       }
       public void acrescentarDVDdeAlugar(DVDdeAlugar d) { ... }
    
       // Imprimir todos os DVDs 
       public void relatório() {
          for (int i=0; i < numeroDVDsAlugar; i++) {
    	      dvdsDeAlugar[i].imprimir();
    	  }
          for (int i=0; i < numeroDVDsVender; i++) {
    	      dvdsDeVender[i].imprimir();
    	  }
       }
    }
    

    A regra da substituição diz que sempre que um programa espera um objeto (por exemplo, como parâmetro de um método, ou em uma atribuição) podemos substituir o objeto por outro objeto que seja instância de uma classe que é filha da classe esperada.

    Deve-se sempre seguir a regra do É-UM para se saber se uma subclasse é apropriada. Por exemplo, um DVDdeAluguel não É-UM DVDdeVender. São coisas diferentes. No entanto, tanto DVDdeAluguel É-UM DVD quanto DVDdeVender É-UM DVD. Vamos então reescrever a nossa hierarquia:

    public class DVD {
       private float preço;
       public void adquirir();
       public void devolver();
       public void recibo();
    }
    
    public class DVDdeVender extends DVD {
       public void adquirir();
       public void devolver();
    }
    
    public class DVDdeAlugar extends DVD {
       private Date dataDevolução;
       public void adquirir();
       public void devolver();
    }
    

    A classe DVD contém todos os dados e funções comuns às duas classes anteriores. Cada classe herda esses membros e ainda acrescenta aquilo que lhe é particular.

    A distinção entre interface e implementação é importantíssima aqui: como a subclasse necessariamente contém a interface da classe pai, garante-se que a substituição é possível.

    Outra regra importante de se lembrar é que pode-se sempre atribuir uma instância de uma subclasse a uma variável da superclasse, mas nunca se pode fazer o contrário. Ou seja,

        DVD d1 = new DVDdeAlugar();  // OK
        DVDdeAlugar d2 = new DVD();  // Não é permitido.
    
    O princípio da substituição nos permite reescrever a classe ColeçãoDeDVDs:
    public class ColeçãoDeDVDs {
       private DVD[] dvds;
       private int numeroDVDs;
       
       public void acrescentarDVD(DVD d) {
       		  dvds[numeroDVDs] = d;
    		  numeroDVDs++; 
       }
    
       // Imprimir todos os DVDs 
       public void relatório() {
          for (int i=0; i < numeroDVDs; i++) {
    	      dvds[i].imprimir();
    	  }
       }
    }
    
    Usando o princípio da substituição, podemos usar a classe ColeçãoDeDVDs para acrescentar ambos os tipos de DVD:
    public static void main() {
       ColeçãoDeDVDs c;
       
       c.acrescentarDVD(new DVDdeAlugar());
       c.acrescentarDVD(new DVDdeVender());
       c.relatório();
    }
    

    Polimorfismo é um termo usado no mundo OO para se referir à substituição de classes por subclasses.

    Classes abstratas e interfaces

    O procedimento de devolução de DVDs vendidos e alugados é bem diferente. O que colocar na classe DVD, então? Uma classe pode ter métodos sem implementação, apenas interface. Uma classe que tenha algum método sem implementação é chamada classe abstrata

    public abstract class DVD {
       private float preço;
       public abstract void adquirir();
       public abstract void devolver();
       public void recibo();
    }
    

    Classes abstratas não podem ser instanciadas e servem apenas como base para substituição.

    Uma interface (em Java) é uma classe que não tem nenhum dado e em que todos os métodos são abstratos. Herdar de interfaces tem uma sintaxe própria, sendo um tipo especial de herança, porque uma mesma classe pode herdar de várias interfaces.

    No exemplo abaixo, Cloneable é uma interface da biblioteca padrão Java que define um único método, clone(), e Comparable é uma interface que define o método compareTo().

    public class DVDdeAlugar extends DVD implements Cloneable, Comparable {
        ...
    }
    
    Pense: o que se ganha em declarar que se está implementando uma interface? Por que não simplesmente implementar clone() e compareTo sem declarar que se está implementando a interface?

    (Aula anterior) --- (Próxima aula)