Herança

A herança está ligada à reutilização de código e ao polimorfismo. Analise o código abaixo:

package devmedia;
class Funcionario extends Object{}
class SubGerente extends Funcionario{}
class Chefe extends SubGerente{}
class Dono extends Chefe{}

public class Executavel {

   public static void main(String args[]){
       Funcionario f = new Funcionario();
       SubGerente s = new SubGerente();
       Chefe c = new Chefe();
       Dono d = new Dono();
       
       if(d instanceof Dono){
           System.out.println("Dono é um Dono");
       }
       if(d instanceof Chefe){
           System.out.println("Dono é um Chefe");
       }
       if(d instanceof SubGerente){
           System.out.println("Dono é um SubGerente");
       }
       if(d instanceof Funcionario){
           System.out.println("Dono é um Empregado");
       }
       if(d instanceof Object){
           System.out.println("Dono é um Object");
       }
   }
   
}

Qualquer classe em Java é uma filha (subclasse) de Object, exceto a própria, obviamente.
Desse modo, sempre será disponível um método clone(), um metodo notify(), equals(), wait(), além de outros.
Os inventores de Java previram corretamente que seria muito comum os programadores quererem comparar instâncias das suas classes para verificar se são iguais, ou seja, se possuem o mesmo endereço.
É essencial entender que as 2 razões para utilizar a herança são: promover o polimorfismo  e proporcionar a reutilização de código.
Provavelmente, você já deve ter passado pela situação de encontrar código duplicado ao fazer uma alteração em um lugar e ter que buscar todos os outros pontos em que exista o mesmo código.
O segundo benefício de uso da herança é proporcionar que as suas classes sejam acessadas poliformicamente.
Vamos supor que se tenha uma classe ListaDePagamento e queira fazer um loop pelos vários tipos de objetos Empregado e chamar exibirCargo() em cada um deles.
A grande funcionalidade do polimorfismo está em poder tratar qualquer subclasse de Empregado como se fosse um Empregado. Sendo assim, na classe ListaDePagamento, pode se escrever um código que fale: “não me importa que tipo de objeto que você é, desde que seja(estenda) Empregado”.

package devmedia;
class Funcionario extends Object{
   public String getCargo(){
       return "Funcionário";
   }
   public int getRemuneracao(){
       return 500;
   }
}
class SubGerente extends Funcionario{
   public String getCargo(){
       return "SubGerente";
   }
   public int getRemuneracao(){
       return 1000;
   }
}
class Chefe extends SubGerente{
   public String getCargo(){
       return "Chefe";
   }
   public int getRemuneracao(){
       return 1500;
   }
}
class Dono extends Chefe{
   public String getCargo(){
       return "Dono";
   }
   public int getRemuneracao(){
       return 2000;
   }
}

public class Codigo {

   public static void main(String args[]){
       Funcionario [] listagem = new Funcionario[4];
       listagem[0] = new Dono();
       listagem[1] = new Chefe();
       listagem[2] = new SubGerente();
       listagem[3] = new Funcionario();
       
       avaliaFuncionario(listagem);
   }

   public static void avaliaFuncionario(Funcionario[] listagem) {
       for(Funcionario f : listagem){
           System.out.println(f.getCargo()+" "+f.getRemuneracao());
       }
   }
   
}

Saída:
Dono 2000
Chefe 1500
SubGerente 1000
Funcionário 500

No exemplo acima, o método recebe um array contendo um Dono, um Chefe, um SubGerente e um Funcionario. Assim, o método em questão pode fazer o processo de qualquer tipo de Funcionario sem se preocupar sobre qual real tipo de classe, em tempo de execução, do objeto que foi passado ao método.
O método avaliaFuncionario() sabe somente que os objetos são um array Funcionario, uma vez que o parâmetro foi declarado desse modo. E utilizar uma variável de referência declarada como do tipo Empregado, independentemente se a variável é uma variável local, um parâmetro de método ou uma variável de instância (atributo), significa que apenas os métodos de Funcionario podem ser invocados para ela.

É-UM

No mundo OO, o conceito de é-um é baseado implementação de interfaces ou na herança de classes. Esse termo é-um é uma maneira de dizer “esse item é um tipo de outro”.
Em Java, o relacionamento é-um é expressado através da palavra-chave extends para herança de classes implements para implementação de interfaces.

TEM-UM

Os relacionamentos tem-um são baseados no uso, ao contrário da herança.
Exemplificando, você pode dizer que um Gerente é-um Empregado e tem-um Endereço.
Os relacionamentos tem-um permitirão que suas classes sejam projetadas seguindo boas práticas de OO, sem que sejam necessárias classe monolíticas, que executem várias funções diferentes.
Quanto mais especializada for uma classe, maior a probabilidade que ela possa ser reutilizada em uma outra aplicação.

ENCAPSULAMENTO

Encapsulamento é o conceito a partir do qual os detalhes da implementação dos métodos de uma classe são ocultados das outras classes.
Isso é adquirido mantendo-se os atributos private e disponibilizando os dados para visualização através de métodos get e proporcionando que os dados seja modificados apenas por métodos set.
Suponha que fosse escrito um código que não estivesse sendo usado de modo correto por outros desenvolvedores. Como se tratamos de Java, você deve lançar uma versão nova da sua classe, que eles poderão substituir em seus programas,  sem modificar nada no código que já existia.
Esse cenário resulta em 2 dos benefícios/promessas da OO, que são: manutenibilidade e flexibilidade. Entretanto, esses benefícios não aparecem de forma automática. O principal modo de conquistá-los é usando o encapsulamento.
Os atributos devem ser declarados como private e o acesso deve ser realizado somente por meio de métodos.

POLIMORFISMO

Uma variável de referência só pode ser de um tipo e, uma vez que a declaração foi feita, esse tipo jamais pode ser alterado, embora o objeto referenciado possa se modificar.
O tipo de uma variável de referência estabelece os métodos que podem ser invocados no objeto, que é referenciado. E isso é o que mais importa. É sobre essa premissa que se alicerça o conceito de sobrescrição de métodos.
Uma variável de referência pode apontar para qualquer objeto de mesmo tipo que a referência declarada, ou pode se referir a qualquer subtipo.
Toda classe não pode estender mais que uma classe. Porém, uma classe pode ter múltiplos ancestrais através da implementação de interfaces.

Na próxima parte do artigo abordaremos conceitos de sobreposição e sobrescrição de métodos.

Leia todos artigos da série