Um paradigma de programação basicamente é como um determinado caminho que os programadores utilizam para resolver problemas, e cada caminho tem uma forma diferente de ver e resolver o problema. Existem diversos paradigmas de programação, porém nesse artigo vamos falar sobre o de orientação a objetos (POO). A POO define ao programador uma visão do que está sendo implementado como se fossem vários objetos que trocam mensagens entre si, ou seja, nela uma classe chamada Veiculo poderia ser vista como um objeto da vida real, um veículo propriamente dito, com seus próprios atributos, como velocidade, combustível e etc. A mesma tende a aproximar definições do mundo real para o virtual, temos mais facilidade de implementar algo real, porque já sabemos como o objeto em questão funciona, o que ele faz, quais suas funções entre outros.

Um dos conceitos mais usados do paradigma de objetos é o polimorfismo, que promove a reutilização contínua dos códigos, ou seja, possibilita algo assumir várias formas. No contexto da programação orientada a objetos, ele nos mostra como um método pode assumir formas diferentes das quais foram implementadas inicialmente e agir de modo que possa ser utilizado por outra classe.

Na Figura 1 temos a classe Veiculo com o método setVelocidade. Como ela será uma classe abstrata, não teremos a Velocidade do veículo em si, porém na classe carro sabemos qual a velocidade média de um carro, logo, usaremos o método setVelocidade para receber a velocidade de um carro. Já a classe Aviao recebe a velocidade média de um avião, que é bem maior do que a de um carro. Podemos perceber que o método setVelocidade está sendo reutilizado e se comportando de maneira diferente dependendo da classe à qual ele está, assim como acontece com o método setpassageiros.

Exemplo de polimorfismo
Figura 1. Exemplo de polimorfismo

A herança é muito utilizada na POO e trabalha lado a lado com o polimorfismo, pois por meio dela podemos fazer com que as classes possam partilhar o que elas possuem. Como em uma árvore hereditária da vida real que temos o membro inicial da árvore, os membros filhos, os membros que são filhos dos filhos e assim por diante. Na orientação a objetos não é diferente, pois para ela o membro inicial é chamado de classe pai ou superclasse, e os herdeiros são conhecidos como classes filhas ou subclasses, tendo em vista a POO, podemos pensar que a classe pai é um molde ou algo que abrange todo o escopo da ideia dela, e as filhas são mais específicas, pois que são criadas a partir do molde (classe pai). Vamos a um exemplo: a classe Veiculo, que no caso será nossa classe pai, abrange um escopo vasto, pois se pensarmos em veículos teremos vários tipos diferentes deles, desde uma moto até um ônibus espacial. Agora, utilizando o paradigma de orientação a objetos, vamos poder ver que qualquer veículo possui uma velocidade e também um dos benefícios do mesmo é possuir um ou mais lugares para carregar pessoas, ou seja, sabemos que todos os veículos, por lei, possuem as características citadas, porém cada um tem a sua especificação própria.

Utilizamos o modificador abstract para evidenciar que uma classe servirá de modelo para as outras e a mesma será abrangente e servirá de molde para as classes que nascerem dela. Ao colocarmos ele, a classe à qual o recebe não poderá ser instanciada, então não podemos criar um objeto nela, o que faz bastante sentido, tendo em vista que a classe da qual estamos falando é generalizada e não incorpora em si algo específico propriamente dito.

Na Figura 2 temos o exemplo do esquema hereditário da classe Veículo.

Exemplo de herança
Figura 2. Exemplo de herança

Veja que podemos entender mais o conceito de uma classe molde: a classe Veiculo dá a ideia inicial de um veículo geral, e as classes filhas (carro e avião) especificam detalhes próprios do veículo em questão e dão a vida a objetos.

Tanto polimorfismo quanto herança são referências no ramo de reutilização de código, pois trabalham em conjunto. Existem dois tipos de polimorfismo que são conhecidos como sobrecarga (overload) e sobreposição (override).

A sobrecarga (overload) consiste em permitir, dentro da mesma classe, mais de um método com o mesmo nome. Entretanto, eles necessariamente devem possuir argumentos diferentes para funcionar. RICARTE (2001) afirma que a escolha de qual método irá ser chamado pelo programa principal dependerá de acordo com o seu tipo de objeto, que será tomada apenas no tempo de execução por meio de ligação tardia.

CARVALHO e TEXEIRA (2012) dizem que a técnica de sobreposição permite reescrever um método em uma subclasse que possua um comportamento diferente do método de mesma assinatura na superclasse. Basicamente a sobreposição (override) possibilita reescrever na classe filha os métodos implementados previamente na classe pai, ou seja, uma classe filha pode redefinir métodos herdados de suas descendentes, mantendo o nome e a assinatura. Na sobreposição é necessário que os métodos tenham a mesma assinatura (tipo de retorno, nome do método, tipos e quantidades de parâmetros), mas com implementações diferentes.

Sobrecarga de métodos (Overload)

A sobrecarga de métodos (overload) é um conceito do polimorfismo que consiste basicamente em criar variações de um mesmo método, ou seja, a criação de dois ou mais métodos com nomes totalmente iguais em uma classe. A Sobrecarga permite que utilizemos o mesmo nome em mais de um método contanto que suas listas de argumentos sejam diferentes para que seja feita a separação dos mesmos.

Para entender melhor a sobrecarga, vamos pensar que estamos implementando uma calculadora simples que some apenas dois valores do mesmo tipo por vez. Nela teremos o método calcula que será sobrecarregado com variações de tipos de soma, como mostra a Listagem 1.

public class calculadora{
 public int calcula( int a, int b){
    return a+b;
  }
  public double calcula( double a, double b){
     return a+b;
  }
   public String calcula( String a, String b){
     return a+b;
}
Listagem 1. Exemplo de métodos sobrecarregados

A classe calculadora possui três métodos que somam dois valores do mesmo tipo, porém, os mesmos possuem o mesmo nome, então, como vamos saber se o programa principal vai chamar o método correto ao convocarmos o calcula()? O programa, ao receber o calcula() com os parâmetros passados, verificará na classe calculadora no tempo de execução qual dos seguintes métodos está implementado para receber o parâmetro e convocará o mesmo. Na Listagem 2 vemos um exemplo completo e sua implementação encontra-se na Figura 3.

public class calculadora{
 public int calcula(int a,int b){
       return a+b;
 }
  public double calcula(double a,double b){
        return a+b;
   }
  public String calcula(String a,String b){
        return a+b;
  }
   public static void main(String args[]){
     calculadora calc= new calculadora();
     System.out.println(calc.calcula(1,1));
    System.out.println(calc.calcula(2.0,6.1));
    System.out.println(calc.calcula("vi","ram?"));
  }
}
Listagem 2. Exemplo da passagem de parâmetros para os métodos no main
Resultado do código implementado na Listagem 2
Figura 3. Resultado do código implementado na Listagem 2

Podemos perceber que os métodos sobrecarregados foram convocados corretamente pelo programa principal, logo vemos a importância de manter listas de argumentos diferentes para que a sobrecarga de métodos seja realizada corretamente, pois de outra forma não haveria como o programa principal realizar a diferenciação e seleção deles. Os métodos chamados são ligados por ligação tardia, logo, se o programa encontrar dois métodos com argumentos iguais ele não saberá qual será selecionado para a chamada e haverá um erro em seu programa.

A sobrecarga é muito utilizada em construtores, pois esses consistem em linhas de código que serão sempre executadas quando uma classe for instanciada. Entende-se instanciada quando criamos um objeto a partir dela. Por regra, o programa cria um construtor sem implementação para cada classe criada que, no caso, será o construtor padrão, porém podemos criar quantos construtores acharmos necessários. Podemos entender os construtores como uma base inicial que os objetos terão ao serem criados. Por base os construtores devem possuir o mesmo nome que a classe na qual eles estão. Na classe calculadora temos um construtor padrão sem implementação, porém não o vemos.

Vamos implementar a nossa classe calculadora com atributos e a sobrecarga de construtores conforme a Listagem 3.

public class calculadora{
 
  private String modelo;
  private String marca;
  private String uso;
 
  //Sobrecarga de construtores.
  public calculadora(){
    // esse é o construtor padrão que o programa cria para todas as classes.
  }
  public calculadora(String marca,String modelo){
           this.marca=marca;
           this.modelo=modelo;
  }
 
  public calculadora(String marca,String modelo,String uso){
           this.marca=marca;
           this.modelo=modelo;
           this.uso=uso;
  }
  public int calcula(int a,int b){
       return a+b;
  }
  public double calcula(double a,double b){
        return a+b;
  }
  public String calcula(String a,String b){
         return a+b;
  }
  public static void main(String args[]){
   calculadora calc= new calculadora("optpex","N110","Empresarial");
   calculadora cald= new calculadora("Zion","Neo1");
   System.out.println(calc.calcula(900,1000));
   System.out.println(calc.calcula(99.0,100.1));
   System.out.println(calc.calcula("Sobrecarga de "," construtores"));
   System.out.println("calculadora 1 Marca: "+calc.marca+" Modelo: 
   "+calc.modelo+" Uso: "+calc.uso);
   System.out.println("calculadora 2 Marca: "+cald.marca+" Modelo: 
   "+cald.modelo);
  }
}
Listagem 3. Exemplo de sobrecarga de construtores

A sobrecarga de construtores tem muito em comum com a sobrecarga de métodos; podemos dizer que o conceito de sobrecarga é sempre o mesmo. Utilizamos o comando this para referenciar o objeto no qual estamos, por exemplo, no caso da primeira calculadora (a calc) passamos como parâmetro a marca “optplex”, ou seja, com esse comando o programa vai entender que estamos falando especificadamente da calc. Podemos ver também o construtor padrão da classe calculadora sem nenhuma implementação, ele foi o que utilizamos sem saber na Listagem 2. Podemos ver o resultado dessa sobrecarga de construtores na Figura 4.

Resultado da sobrecarga de construtores
Figura 4. Resultado da sobrecarga de construtores

Portanto entendemos que a sobrecarga é conceito poderoso do polimorfismo, e a mesma permite ao programador mais facilidade na criação de variações de códigos já criados, poupando-o assim de inventar nomes para cada operação que compõem um mesmo escopo.

Sobreposição de métodos (Override)

A Sobreposição de métodos (override) é um conceito do polimorfismo que nos permite reescrever um método, ou seja, podemos reescrever nas classes filhas métodos criados inicialmente na classe pai, os métodos que serão sobrepostos, diferentemente dos sobrecarregados, devem possuir o mesmo nome, tipo de retorno e quantidade de parâmetros do método inicial, porém o mesmo será implementado com especificações da classe atual, podendo adicionar um algo a mais ou não. Para demonstração usaremos a classe Veiculo que introduzimos o início do artigo, como mostra a Listagem 4.

public abstract class Veiculo{
 
  public String modelo;
  public float velocidade;
  public int passageiros;
  public float combustivel;
 
  public Veiculo(){
  }
  public Veiculo(String m,int p,float c){
     this.modelo=m;
     this.passageiros=p;
     this.combustivel=c;
  }
 
  public void setVelocidade(float v){
         velocidade=v;
  }
  public float getVelocidade(){
           return velocidade;
  }
  public void setPassageiros(int p){
          passageiros=p;
  }
  public int getPassageiros(){
           return passageiros;
  }
 public void acelera(){
        System.out.println("Acelera");
  }
  public void freia(){
   System.out.println("Freia");
  }
}
Listagem 4. Criação da classe Veiculo

Na classe Veiculo implementamos atributos básicos e seis métodos. Para demonstrar a sobreposição de métodos criaremos a classe filha carro que herdará a classe Veiculo, e usamos a palavra-chave extends, que basicamente serve para estender o que há na classe pai para a classe filha na qual a palavra foi posicionada. Utilizamos o super para informar ao programa que deve ser chamado o construtor da classe pai, e usamos o this para fazer uma referência ao contexto que é usado no momento. Por exemplo, criamos o objeto corsa na classe carro e colocamos o número de passageiros próprio do carro corsa, e com o comando this o programa entende que estamos falando especificadamente do corsa no momento.

public class carro extends Veiculo
 private int marcha;
 public carro(){
 }
 public carro(String m,int p,float c)
        super(m,p,c);
   }
  public void setVelocidade(float v) 
       velocidade=v; 

       if(velocidade>20 && velocidade<40){
           marcha=2;                                                               
     }
       if(velocidade<20) 
         marcha=1;      
       }
      
       if(velocidade>40 && velocidade<60){
          marcha=3;
       } 

         if(velocidade>60 && velocidade<70){
           marcha=4;                       
         }
                                                       
         if(marcha>70){
           marcha=5;
         }
   }
  public void acelera(){ 
   setVelocidade(getVelocidade()+2.f);
  }
  public void freia(){   
     setVelocidade(getVelocidade()-2.f); 
   }
   public static void main(String args[]){
          carro corsa= new carro("Hatch",5,50.f);
          carro audi= new carro();
          
          int a,b=10;
         System.out.println("Modelo: "+corsa.modelo+" Total de passageiros: 
         "+corsa.passageiros+" Tanque de combustivel: 
         "+corsa.combustivel+" Litros \n"); 
         System.out.println("Acelerando  o carro\n");
   
     for(a=0;a<=10;a++){
       corsa.acelera();
       System.out.println("marcha: "+corsa.marcha+" Velocidade: 
       "+corsa.getVelocidade());
     }
     System.out.println("\nFreiando o carro\n");
      while(b!=0){
      corsa.freia();
      System.out.println("marcha: "+corsa.marcha+" Velocidade: 
      "+corsa.getVelocidade());
      b--;
    }
  }
}
Listagem 5. Exemplo de sobreposição

Na Listagem 5 criamos a classe carro e dois objetos da mesma. Ela é herdeira da classe Veiculo, logo, os objetos que provirem dela também serão veículos. Podemos observar na classe carro que o método setVelocidade foi reescrito, e na classe pai a implementação do mesmo consiste em apenas passar a velocidade, porém na classe carro além de atribuir, ele também verifica qual a marcha o veículo deve assumir dependendo da velocidade.

Os métodos acelera e freia também foram reescritos com uma velocidade imaginária de aceleração e frenagem de um carro, o que se aplica bastante no conceito do polimorfismo, um método que toma outra forma para se adaptar a classe atual. Na sobreposição devemos nos atentar que o método a ser sobreposto deve possuir nome e lista de argumentos totalmente iguais aos do método inicial, o que podemos ver nos métodos setVelocidade, acelera e freia. O resultado do código da Listagem 5 é apresentado na Figura 5.

Resultado da sobreposição de métodos
Figura 5. Resultado da sobreposição de métodos

O objeto audi serviu para demonstrarmos que podemos criar quantos objetos a partir do molde/classe. Nesse exemplo implementamos apenas a classe carro, porém, poderíamos também ter implementado a classe avião, moto, barco e etc., e só precisaríamos herdar a classe Veiculo e seguir adicionando atributos, reescrevendo métodos e adicionando especificações das mesmas.

Comparativo entre Sobrecarga e Sobreposição

A sobrecarga está ligada a variância de estados de um método, podemos entende-la como um conjunto de opções que o programa principal tem para escolher quando recebe os parâmetros passados pelo usuário. Se tivéssemos apenas um método e não utilizarmos a sobrecarga nele, ao passarmos um parâmetro que não consta nesse método, ocorreriam erros no programa. Já com a sobrecarga conseguimos criar variações de um mesmo método conseguindo assim mais opções de uso para ele, então, para a mesma funcionar os métodos devem conter nomes iguais e listas de argumentos diferentes. Na Figura 6 veremos que o programa principal tem três opções de chamada na classe Calculadora ao invocar o método calcula.

Exemplo das opções de chamada na classe Calculadora
Figura 6. Exemplo das opções de chamada na classe Calculadora

Enquanto a sobrecarga da vida a variação de métodos, a sobreposição possibilita a extensibilidade dos mesmos, pois com ela podemos reescrever métodos criados anteriormente, permitindo assim a criação de versões mais específicas deles. Com a sobreposição podemos pegar um método genérico e transformá-lo em específico, implementando novas funcionalidades pertinentes da classe à qual ele está.

Diferente da sobrecarga, a sobreposição funciona por meio do sistema de herança, e para a mesma funcionar o nome e lista de argumentos dos métodos devem ser totalmente iguais aos da classe herdada. Na Figura 7 podemos ver a herança entre a classe Calculadora e CalcuradoraCientifica, e também a sobrescrita do método calcula.

Exemplo de sobreposição
Figura 7. Exemplo de sobreposição

Portanto podemos identificar esses dois conceitos como grandes ases da reutilização de códigos em orientação a objetos, cada um com sua função e regra própria.

Referências:
  • CAMPOS, M, L. Modelização de domínios de conhecimento: uma investigação de princípios fundamentais. Revista ciência da informação versão v.33 n.1. Brasília 2004.
  • RICARTE, I,L. Programação Orientada a Objetos: Uma Abordagem com Java. Faculdade de engenharia elétrica e de computação UNICAMP. Campinas 2001.
  • CARDELLI, L.; WEGNER, P. On Understanding Types, Data Abstraction, and Polymorphism. ACM Computing Surveys (CSUR). vol.17, pag. 471 – 524. 1985.
  • CARVALHO, V; TEXEIRA, G. Programação orientada a objetos: Curso técnico de informática IFES. Colatina 2012.