POLIMORFISMO
O conceito de Herança e Polimorfismo andam juntos, pois o polimorfismo assume a ideia inversa da herança, onde a subclasse pode assumir as funcionalidades e características de sua superclasse.
Imagine, por exemplo, as camisas de um time. Elas possuem formas e estilos diferentes da camisa de outro time, mas, apesar destas diferenças, elas têm a mesma funcionalidade, ou seja, são formas diferentes de camisas para representar o time nelas (cores, símbolos, etc.).
Suponha uma classe de Conta Bancária Simples, chamada ContaSimples e que tenha os métodos donoConta() e verSaldo() implementados. Agora usa-se a herança para a classe Poupanca, que vai herdar todos os métodos da classe ContaSimples, e adicionar um novo método denominado Juros() somente na classe Poupanca. Para isso é usada a propriedade da Herança. Como a expressão Polimorfismo indica seu próprio significado, ou seja, poli - várias ou muitas, e morfismo - forma, nesta implementação sugerida, o método verSaldo() poderá ser polimórfico e apresentar o saldo de uma conta, seja ela ContaSimples ou Poupanca. Este tipo de implementação é possível devido a propriedade Polimorfismo, pois foi usado um mesmo método para trabalhar com dois tipos de contas diferentes.
Na representação gráfica anterior está sendo mostrada a hierarquia destas classes, onde o polimorfismo consiste na inversão da propriedade da herança, indicadora de que cada subclasse pode assumir as características e funcionalidades de sua superclasse, além de se especializar em suas próprias características. De maneira contrária, uma subclasse pode se concentrar somente nas características de implementação de sua superclasse, ou seja, generalizando-se (polimorfismo).
Observe que no diagrama anterior existem 2 superclasses, mas concentre-se na análise somente da superclasse ContaSimples, quando seu programa precisar de um dado comum entre suas 2 subclasses (Poupanca e Investimento), por meio de um objeto de ContaSimples isso será possível através da propriedade da herança. Porém, se precisar de um determinado cálculo de juros, disponível somente para poupança, durante o período de tempo que um valor em dinheiro ficou aplicado em uma destas contas simples, certamente estará se referindo a um objeto da subclasse Poupanca. Assim, se conclui que todas estas subclasses são também ContaSimples, generalizando qualquer uma das duas.
Isso indica que a necessidade de subir no diagrama de hierarquia de classes utiliza a propriedade do polimorfismo (generalização), enquanto que descer neste mesmo diagrama configura o uso da propriedade da herança (especialização).
/** * Atributos: conta e saldo * Métodos: setNome(String), getNome(), setSaldo(float), getSaldo() */ public class ContaSimples { // atributos private String conta; private Float saldo=100F; // métodos public void setConta(String contaParametro) { this.conta = contaParametro; } public String getConta(){ return conta; } public void setSaldo(float saldoParametro) { this.saldo = saldoParametro; } public float getSaldo(){ return saldo; } } /** * Atributos: Nenhum * Métodos: calculaJuros(float) */ public class Poupanca extends ContaSimples { // métodos public void calculaJuros(Poupanca contaPoupanca) { float juros = contaPoupanca.getSaldo() * 0.02F; contaPoupanca.setSaldo((contaPoupanca.getSaldo() + juros)); } } /** *Síntese * Objetivo: Gerenciar contas bancárias * Entrada: Só atribuições de código e saldo de conta * Saída: Código da conta e seu saldo */ public class GerenciaContas{ public static void main(String []Args){ ContaSimples conta = new ContaSimples(); conta.setConta("34147-1"); conta.setSaldo(100F); System.out.println("CONTA = " + conta.getConta()); verSaldo(conta); Poupanca poupanca = new Poupanca(); poupanca.setConta("34147-P"); poupanca.setSaldo(100F); System.out.println("\nPOUPANÇA = " + poupanca.getConta()); poupanca.calculaJuros(poupanca); verSaldo(poupanca); } // método que mostra saldo de qualquer tipo de conta public static void verSaldo(ContaSimples contas){ System.out.println("Saldo = " + contas.getSaldo() + " reais"); } }
Observe que este exemplo estará implementando somente uma das contas específicas (Poupança) para ilustrar o polimorfismo. Na primeira classe ContaSimples são atribuídos alguns métodos básicos para definir uma conta simples, com os atributos devidamente protegidos para que não exista possibilidade de manipulação inadequada de seus dados (qualificador private). Na segunda classe, Poupanca, são herdados todos os atributos e métodos da classe ContaSimples, e se adiciona um novo método com identificador calculaJuros, se diferenciando da classe ContaSimples que não possui este método. Em seguida, são declarados os objetos conta e poupanca, cada um respectivo a sua própria classe, sendo acionado o método verSaldo para os dois objetos criados nesta implementação polimórfica, pois este último método citado é usado pela instância de conta e de poupanca (objetos diferentes).
Não é permitido declarar na mesma classe dois métodos com o mesmo nome e os mesmos argumentos (mesma assinatura do método). Isto não tem nenhum sentido, pois os métodos são identificados por sua assinatura. Se isso fosse permitido haveria uma grande confusão, pois como se poderia determinar, precisamente, qual método acionar?
Entretanto, uma das finalidades de usar o polimorfismo é atribuir à algumas subclasses novas funcionalidades, inclusive pela mesma assinatura de seus métodos. Isto é possível acrescentando-se novos métodos às subclasses, sobrepondo-se métodos por meio da definição de novas lógicas de programação em métodos com a mesma assinatura já existentes em suas superclasses. Por exemplo, considere a classe Computador a seguir:
/** * Atributos: ligado * Métodos: desligar() */ public class Computador { //atributo protected boolean ligado; //métodos public void desligar() { System.out.println("Desligando o computador!"); ligado = false; } }
A codificação anterior permite que o computador seja desligado, através da chamada do método desligar(). No entanto, isso pode não ser muito seguro, pois um computador poderia ser desligado ainda quando estivesse executando algum programa. Nesse caso, pode-se evitar um problema derivando a classe computador do seguinte modo:
/** *Síntese * Objetivo:Fazer a segurança do computador ao ser desligado * Entrada: Sem entrada * Saída: Situação do computador */ public class ComputadorSeguro extends Computador { public static void main(String []Args) { ComputadorSeguro computador = new ComputadorSeguro(); computador.ligado = true; // ligando o computador computador.desligar(); } public void desligar() { if (ligado) System.out.println("Há programas rodando. Não desligue!"); else { System.out.println("Computador será desligado."); ligado = false; } } }
Com esta implementação um objeto da classe ComputadorSeguro somente será desligado quando não tiver programas rodando (sendo executados). A sobreposição somente acontece quando o novo método é declarado com exatamente o mesmo nome e lista de argumentos que o método existente na superclasse (mesma assinatura), como já foi abordado no conteúdo de herança. Além disso, a sobreposição não permite que o novo método tenha mais proteções do que o método original. No exemplo anterior, como o método desligar() foi declarado como public na superclasse, este não pode ser declarado private na subclasse. A retirada do método desligar() da classe ComputadorSeguro resultaria no acionamento do mesmo método existente na classe Computador (superclasse de ComputadorSeguro) por padrão.
Na programação de computadores geralmente são criados vários métodos com o mesmo nome que atuam de maneira diferente em sua execução (outras lógicas e outros processamentos). Normalmente, estes métodos possuem somente o identificador igual e envolvem tipos e quantidades diferentes de parâmetros. Estas características sobrecarregam os métodos e não os sobrepõem, pois suas assinaturas são diferentes. Por exemplo, cria-se um método que realize a soma de dois parâmetros de entrada, mas não se sabe se a soma será de inteiros ou de reais. Assim, elabora-se um método para o caso dos reais e outro com o mesmo nome para o caso dos inteiros.
Este tipo de implementação estará sobrecarregando o identificador igual de ambos os métodos, mas não o estará sobrepondo.
/** * Atributos: Nenhum * Métodos: soma(int, int), soma(float, float) */ public class Calculadora { public int soma(int valor1, int valor2) { return (valor1 + valor2); } public float soma(float valor1, float valor2) { return (valor1 + valor2); } } /** *Síntese * Objetivo: Somar usando a calculadora * Entrada: Sem entrada (só atribuições) * Saída: Resultado das diferentes somas */ public class UsaCalculadora { public static void main(String[] args) { int operacao1; float operacao2; Calculadora somador = new Calculadora(); operacao1 = somador.soma(2, 3); operacao2 = somador.soma(0.24F, 12.55F); System.out.println("A soma dos inteiros 2 e 3 é: " + operacao1); System.out.println("Somando 0.24 e 12.55, tem: " + operacao2); } }
No exemplo de código anterior, não se conhece os valores a serem somados, se eles serão inteiros ou reais, por isso foram elaborados dois métodos para que o programa possa fazer a soma para estas duas possíveis situações (soma entre inteiros ou entre reais), conforme sejam os valores a serem somados.
A sobrecarga permite a implementação de vários métodos com o mesmo nome (identificador), mas com assinaturas distintas (em termos de tipos de retorno e listas de argumentos). Sendo assim, em tempo de execução, o método adequado à lista de parâmetros é executado. Para exemplificar esta situação é apresentada a relação a seguir, que mostra algumas variações nas assinaturas do método println (System.out.println()), sobrecarregando-o.
Na relação anterior, as assinaturas deste método variam em função, basicamente, do tipo do argumento considerado (os parâmetros). Então, é possível esclarecer que, basicamente, a sobrecarga utiliza o nome (identificador) dos métodos idênticos, mas com parâmetros diferentes, sendo assim criados novos métodos com novas assinaturas, enquanto que na sobreposição é utilizada a mesma assinatura do método criado na superclasse, sendo este sobreposto durante o uso de tal subclasse.
Também é válido e comum sobrecarregar os métodos construtores. Diante disso, pode-se definir diferentes construtores nas classes em tempo de execução, da mesma forma que acontece com outros métodos, o construtor é selecionado, de acordo com os argumentos considerados.
Os construtores são métodos com o mesmo nome de sua classe, que servem para definir o valor das variáveis da classe quando seus objetos forem criados e disponibilizados na memória do computador. Observe o exemplo de código a seguir:
/** * Atributos: umaVariavel e outraVariavel * Métodos: UmaClasse(byte, byte) */ public class UmaClasse { private byte umaVariavel, outraVariavel; //Agora cria-se o construtor com o mesmo nome da classe UmaClasse(byte x, byte y) { umaVariavel = x; outraVariavel = y; } }
Com o construtor já definido, atribui-se valores às variáveis da classe no momento em que se cria o objeto, inicializando os atributos desta classe.
UmaClasse objeto1 = new UmaClase(10,12); // exemplo de acionamento
Neste caso a variável umaVariavel do objeto objeto1 terá o valor 10 e a variável outraVariavel do mesmo objeto receberá o valor 12.
Por outro lado, é possível que diferentes objetos que tenham base na mesma classe não necessitem usar o mesmo construtor. Por exemplo, imagine uma classe de clientes, com nome, idade e nome da empresa como atributos desta classe. Todos objetos a serem criados por esta classe podem não possuir os dados iniciais para serem completamente armazenados na memória, podendo a sobrecarga tratar desta possível situação adequadamente.
Na programação se deve buscar um equilíbrio entre usabilidade (muitos construtores) e código claro (poucos construtores). Um programador eficiente deve ser capaz de declarar somente aqueles construtores que realmente serão usados, sem cair na tentação de criar construtores para todos os possíveis casos, por mais incoerentes que sejam.
/** *Síntese * Atributos: nome, nomeEmpresa e idade * Métodos: Cliente(), Cliente(String, String, int), Cliente(String, String), * getNome(), setNome(String), getNomeEmpresa(), setNomeEmpresa(String), * getIdade() e setIdade(int) */ public class Cliente { private String nome; private String nomeEmpresa; private int idade; // Construtor padrão caso não se conheçam os valores iniciais public Cliente() { // inicialização com valores padrões nos atributos da classe } // Outro construtor usado quando todos os dados são conhecidos public Cliente(String nomeValor, String empresaValor, int anos) { this.nome = nomeValor; this.nomeEmpresa = empresaValor; this.idade = anos; } /* Outro construtor com nome do cliente e da empresa conhecidos, mas sem conhecimento da idade */ public Cliente(String nomeValor, String empresaValor) { this.nome = nomeValor; this.nomeEmpresa = empresaValor; } // Métodos de encapsulamento desta classe public String getNome() { return nome; } public void setNome(String nome) { this.nome = nome; } public String getNomeEmpresa() { return nomeEmpresa; } public void setNomeEmpresa(String nomeEmpresa) { this.nomeEmpresa = nomeEmpresa; } public int getIdade() { return idade; } public void setIdade(int anos) { this.idade = anos; } } /** *Síntese * Objetivo: Cadastrar clientes * Entrada: nome, idade e nome da empresa * Saída: nome, nome da empresa e idade (se for conhecida) */ public class CadastraCliente extends Cliente { public static void main(String[] args) { String nomeAux, empresaAux; int idadeAux, qtdeClientes = 0; // Criação de objeto da classe Cliente conhecendo todos valores Cliente paulo = new Cliente("Paulo","Empresa Mate",55); qtdeClientes++; // Criação de objeto da classe Cliente onde não se conhece idade Cliente julia = new Cliente("Julia","Construtora Helix"); qtdeClientes++; nomeAux = paulo.getNome(); empresaAux = paulo.getNomeEmpresa(); idadeAux = paulo.getIdade(); System.out.println(nomeAux + ", trabalha na " + empresaAux + " e possui " + idadeAux+" anos."); nomeAux = julia.getNome(); empresaAux = julia.getNomeEmpresa(); System.out.println(nomeAux + ", trabalha na " + empresaAux + "."); System.out.println("\n\nExistem " + qtdeClientes + " clientes na empresa."); } }
No exemplo anterior, a classe Cliente possui alguns métodos construtores que analisam os dados que foram fornecidos para criação de um objeto na memória do computador. Por fim, são indicados quantos clientes estão na empresa para atendimento, sendo a execução deste programa anterior representado na figura a seguir.
Com intuito de apoiar o aprendizado sobre Orientação a Objeto, sugere-se assistir a videoaula para o aperfeiçoamento no conhecimento deste conteúdo. |
Atividade de Fixação
No intuito de fixar a aprendizagem iniciada por meio deste módulo e verificar como seu entendimento sobre este conteúdo está, estão sendo sugeridos alguns exercícios de fixação para serem resolvidos. Clique no link de exercícios ao lado, pois será por meio dela iniciada a lista de exercícios sobre os conteúdos estudados até este momento nesta disciplina.