Métodos: módulos de programa em Java – Parte 1: À medida que os problemas computacionais vão se tornando maiores e mais complexos, sempre é possível simplificar a solução dividindo o programa em partes menores, chamadas de módulos (em Java, métodos). Um módulo pode ser definido como um conjunto de instruções da linguagem que realizam alguma tarefa, constituindo um procedimento algorítmico, com uma função bem definida e o mais independente possível em relação ao restante do programa. A sistemática de dividir os programas em módulos surgiu no final da década de 1960. O principal objetivo da modularização é permitir gerenciar a complexidade no desenvolvimento de programas de grande porte. Com a subdivisão de programas complexos em módulos, algumas vantagens são conquistadas como a legibilidade (trechos de código mais simples), a manutenibilidade (favorece a detecção e correção de erros) e a produtividade (facilita a reutilização de software).


Em que situação o tema é útil:
À medida que os problemas vão se tornando maiores e mais complexos, sempre é possível simplificar dividindo a solução em partes menores, chamadas de subprogramas (em Java, métodos). Cada parte menor do problema tem uma implementação mais simples, favorecendo a legibilidade e a manutenibilidade do subprograma.

Com o avanço da tecnologia e o aumento na capacidade dos computadores, problemas mais complexos passaram a ser resolvidos pela máquina, provocando uma crise no processo de desenvolvimento de programas (software) que não apresentou uma evolução comparável.

A “crise do software” foi o termo utilizado, já no final dos anos 60, para expressar as dificuldades frente ao rápido crescimento da demanda por software, da complexidade dos problemas a serem resolvidos e da inexistência de técnicas estabelecidas para o desenvolvimento de sistemas. Uma das primeiras e mais conhecidas referências ao termo foi feita por Edsger W. Dijkstra, em 1972, no Prêmio Turing da Association for Computing Machinery, no manuscrito intitulado “The Humble Programmer”.

Infelizmente os problemas encontrados nas etapas do processo de desenvolvimento de software continuam atormentando a vida dos projetistas até hoje. A crise se manifesta de várias formas, como a baixa produtividade dos programadores (projetos ultrapassando os prazos e orçamentos), a falta de uma metodologia formal para o desenvolvimento de software e os códigos de baixa qualidade (programas literalmente sem a possibilidade de serem mantidos).

Com base nisso, neste primeiro artigo sobre programação modular em Java, será estudado como organizar a solução de problemas computacionais em pedaços, enfatizando como declarar e utilizar módulos para facilitar o projeto, implementação e manutenção de grandes programas.

Elementos chaves da programação estruturada

A programação estruturada é uma forma de programação de computadores que estabelece uma disciplina no desenvolvimento de algoritmos, independentemente da complexidade do problema e da linguagem de programação na qual a solução será codificada. Essa disciplina orienta os programadores na criação de estruturas simples em seus programas, usando um número restrito de mecanismos de codificação com especial destaque para a subprogramação (ou modularização).

À medida que os problemas vão se tornando maiores e mais complexos, sempre é possível simplificar a solução dividindo o programa em partes menores, chamadas subprogramas. Um subprograma é o nome dado a um trecho de um programa mais complexo e que, em geral, encerra em si próprio um pedaço da solução de um problema maior (o programa a que ele está subordinado). Procedimento, função, módulo (estrutura modular), e métodos (orientação a objetos), são sinônimos usados na Engenharia de Software para o conceito de subprograma.

Na programação estruturada, o “método dos refinamentos sucessivos” é uma “sistemática” de abordagem útil no projeto e na implementação de softwares. Partindo-se de um dado problema, para o qual se deseja encontrar um programa de solução, deve-se procurar subdividi-lo em problemas menores e consequentemente de solução mais simples (dividir para conquistar). Alguns destes problemas menores (subproblemas) terão solução imediata (na forma de um subprograma ou método) e outros não. Os subproblemas para os quais não for possível encontrar uma solução direta devem ser novamente subdivididos. Assim, o processo é repetido até que se consiga encontrar um subprograma para solucionar cada um dos subproblemas definidos. Então, o programa de solução do problema original será composto pela justaposição dos subprogramas usados para solucionar cada um dos subproblemas em que o problema original foi decomposto.

Com a subdivisão de programas complexos, algumas vantagens são conquistadas:

  1. cada parte menor tem um código mais simples;
  2. facilita o entendimento uma vez que os subprogramas podem ser analisados como partes independentes (legibilidade);
  3. códigos menores são mais facilmente modificáveis para satisfazer novos requisitos do usuário e para correção deerros (manutenibilidade);
  4. simplificação da documentação de sistemas;
  5. desenvolvimento de software por equipes de programadores; e,
  6. reutilização de software através de bibliotecas de subprogramas (produtividade) – na linguagemC, sob a forma dos arquivos de cabeçalhos (.h) e na linguagem Pascal, através das unidades de código (unit).

A reutilização de software pode ser apoiada pela construção de bibliotecas com conjuntos de subprogramas destinados a solucionar tarefas bastante corriqueiras, como, por exemplo, validação de CPF e CNPJ, operações com data, cálculos matemáticos como médias e percentuais, entre outras. No desenvolvimento de novos sistemas, deve-se basear sua concepção ao máximo nos subprogramas disponíveis nas bibliotecas, de modo que a quantidade de código realmente novo a ser desenvolvido seja minimizada. A reutilização de subprogramas busca aumentar a qualidade e a produtividade no desenvolvimento de software, objetivando a economia de tempo e trabalho, benefícios técnicos que trazem vantagens financeiras.

Programando em Java

No desenvolvimento de software o enfoque estruturado sugere a construção de sistemas de informação baseada na compreensão desses sistemas como um conjunto de subprogramas que, por sua vez, executam processos sobre os dados. Já o enfoque orientado a objetos consiste na observação do mundo real como uma coletânea de objetos que interagem entre si, com características próprias, representadas por seus atributos (dados) e operações (processos).

Java é uma linguagem de programação orientada a objetos em que o principal bloco de construção de todos os sistemas de software é a classe. Sob essa perspectiva, os dados e os processos fazem parte, ou são encapsulados, nesse elemento básico de programação.

A sintaxe (forma de escrever uma sentença corretamente) Java usada na definição de uma classe está dividida em quatro partes, identificadas na Figura 1, que são:

  1. modificador;
  2. palavra-chave class;
  3. nome da classe; e,
  4. corpo da classe.

O modificador (opcional) especifica a acessibilidade da classe; se presente, pode ser uma combinação de public e abstract ou final. Neste momento, é necessário entender que uma classe declarada com um modificador de acesso public, indica que todo o conteúdo (atributos e métodos) público da classe pode ser utilizado livremente (sem restrições). A palavra-chave class define que trata-se da declaração de uma classe Java. O nome da classe é o identificador da classe e deve ser um identificador válido para a linguagem. No corpo da classe residem as declarações de todos os atributos e métodos delimitados por chaves de abertura ({) e fechamento (}).

Sintaxe Java na definição de classes

Figura 1. Sintaxe Java na definição de classes.

Módulos de programa em Java

Segundo DEITEL (2005, pág. 165), há três tipos de módulos em Java: métodos, classes e pacotes. Os métodos, denominados funções ou procedimentos nas linguagens de programação estruturada, são serviços implementados na forma de um conjunto de instruções da linguagem (procedimentos algorítmicos) que realizam alguma tarefa específica e podem, como resultado, retornar um valor. Uma boa prática de programação é manter a funcionalidade de um método simples, desempenhando uma única tarefa.

A sintaxe Java usada na definição de um método está dividida em cinco partes, identificadas na Figura 2, que são:

  1. modificador;
  2. tipo do valor de retorno;
  3. nome do método;
  4. lista de parâmetros; e,
  5. corpo do método.

O modificador (opcional) especifica a acessibilidade do método; se presente, pode ser uma combinação dos modificadores de acesso: public, protected ou private; abstract ou final; e, static. O tipo do retorno é um indicador para o valor de retorno. Deve-se usar a palavra reservada void quando o método não possuir um valor de retorno (procedimento). O nome do método é o identificador usado para referenciá-lo em uma sentença de chamada de método. Os parâmetros (opcional) são representados por uma lista de parâmetros, separados por vírgulas, onde cada parâmetro obedece à forma tipo nome, tal como na declaração de variáveis. Os parâmetros são utilizados para receber os valores (argumentos) fornecidos ao método pela respectiva chamada. No corpo do método deverá ser implementado o trecho de código Java que realiza a tarefa, delimitada por chaves de abertura ({) e fechamento (}).

Sintaxe Java na definição de métodos

Figura 2. Sintaxe Java na definição de métodos.

Para DEITEL (2005), a experiência mostra que a melhor maneira de desenvolver e manter um programa grande é construí-lo em partes pequenas e simples, chamadas de métodos. Um programa maior dividido em módulos, chamado de “programa modular”, pode apresentar quantos módulos forem necessários ou convenientes, dos quais o método main(), principal em inglês, representa o “ponto inicial” da execução.

Quando o Java encontra uma chamada de método em uma aplicação Java modular, por exemplo, na chamada do método soma() implementada através da instrução:

System.out.printf("5 + 3 = %d\n", <strong>soma(5, 3)</strong>);

Imediatamente o fluxo de execução é transferido para o método chamado, iniciando a execução a partir do primeiro comando implementado no corpo desse método. Quando o último comando do corpo do método ou o comando return for executado, o Java transfere desta vez a execução para o comando que segue imediatamente a chamada do método.

A Figura 3 apresenta uma “classe Java principal”, nome dado a uma classe Java que possui o método main(), com três métodos:

  1. main() – módulo principal da aplicação, que corresponde ao ponto inicial da execução do código da classe (módulo chamador);
  2. soma () – módulo chamado para calcular e retornar a soma de dois números inteiros; e,
  3. sub() – módulo chamado para calcular e retornar a subtração de dois números inteiros.
Organização deuma classe Java dividida em métodos

Figura 3. Organização deuma classe Java dividida em métodos.

De acordo com DEITEL (2005), os métodos permitem que o programador modularize um programa separando suas tarefas em unidades autocontidas. A Listagem 1 apresenta uma classe Java principal dividida em três métodos:

  1. main() – módulo principal da aplicação, que corresponde ao ponto inicial da execução do código da classe (módulo chamador);
  2. tabuada() – módulo chamado para executar a tabuada de um número; e,
  3. direitos() – módulo chamado para mostrar os direitos autorais da aplicação.

Listagem 1. Aplicação Java dividida em métodos.


public class Exemplo1 {

  public static void main(String[] args) {
    tabuada(7);
    tabuada(8);
    direitos();
  }

  public static void tabuada(int n) {
    int i;

    System.out.printf("+--Resultado--+\n");
    for (i=1; i<=10; i++) {
      System.out.printf("| %2d * %d = %2d 
      |\n", i, n, (i*n));
    }
    System.out.printf("+-------------+\n\n");
  }

  public static void direitos() {
    System.out.printf("Copyright (C) 
    Prof. Omero Francisco Bertol.\n");
  }

}

No método main() da aplicação Java apresentada na Listagem 1, foram realizadas duas chamadas ao método tabuada() enviando como argumentos os valores 7 e 8, respectivamente, e uma chamada ao método direitos(), que não necessita de argumentos.

O método tabuada(), ainda na Listagem 1, foi implementado com um parâmetro inteiro identificado por n. Esse parâmetro recebe uma “cópia” do valor do argumento enviado através da instrução de chamada correspondente. No corpo desse método foi implementado um processo de repetição (for) para montar a tabuada de n. Já o método direitos() foi desenvolvido para exibir no monitor de vídeo, através de uma operação de saída usando o método printf(), o nome do autor que detém os direitos autorais da aplicação.

Na Figura 4 pode-se observar a execução da classe Exemplo1, desenvolvida para exemplificar uma aplicação Java dividida em métodos.

Executando aclasse Exemplo1 dividida em métodos

Figura 4. Executando aclasse Exemplo1 dividida em métodos.

Para realização de uma tarefa em um programa é necessário um método. Os métodos são blocos de código Java que pertencem a uma classe e definem as ações a serem tomadas em diversos momentos da execução de um programa.

A execução de um método para realizar uma tarefa ocorre quando um módulo envia uma “mensagem”, conhecida como “chamada de método”, com informações que instruem como o método deverá executar sua tarefa. Essas informações são conhecidas como parâmetros.

Parâmetros

Os parâmetros são canais pelos quais se estabelece a transferência de informações (constantes, variáveis ou expressões) entre um método e o método que realizou a chamada (o método main() ou outro método qualquer) funcionando como variáveis de entrada de dados.

Um método deve ser definido com uma lista de zero ou mais parâmetros declarada no seu cabeçalho. Essa lista é denominada de lista de parâmetros formais, lista de parâmetros ou simplesmente parâmetros. Quando um método é definido com mais de um parâmetro, é necessário especificar o tipo e o nome de cada parâmetro individualmente e separá-los por vírgula. Por exemplo:

  1. declarando o cabeçalho do método direitos(), apresentado na Listagem 1, que não possui parâmetros:
    // mostrar os direitos autorais da aplicação
    public staticvoid direitos() { … }
  2. declarando o cabeçalho do método tabuada(), apresentado na Listagem 1, que tem um parâmetro do tipo inteiro, identificado por n:
    // montar a tabuada de “n”
    public staticvoid tabuada(int n) { … }
  3. declarando o método IMC(), que no cabeçalho relaciona dois parâmetros dotipo real de dupla precisão, identificados por pce alt, respectivamente:
    // calcular o Índice de Massa Corporal (IMC): peso corporal dividido pela altura ao quadrado
    public staticdouble IMC(double pc, doublealt) {
     return(pc / (alt * alt));
    }
    
  4. declarando o método GEB(), que no cabeçalho relaciona quatro parâmetros, o primeiro do tipo caractere, o segundo do tipo real de dupla precisão e o terceiro e quarto do tipo inteiro, identificados por sexo, pc, alt e idade, respectivamente:
    
    // calcular o Gasto Energético Basal (GEB): consumo diário básico definido em função do sexo,
    peso corporal, altura e idade
    public static
    double GEB(char sexo, double
    pc, int alt, int
    idade) {
     if ((sexo == 'M') || (sexo == 'm'))
    return(66.47 + (13.75 * pc) + (5 * alt)- (6.76 * idade));
     else
     return(655.1 + (9.56 * pc) + (1.85 *alt) - (4.67 * idade));
    }

Se um método for implementado recebendo parâmetros, os mesmos deverão ser colocados no interior dos parênteses na instrução de chamada, separados por vírgulas; caso contrário, os parênteses deverão permanecer vazios. Esses valores são denominados de parâmetros reais, parâmetros efetivos ou simplesmente argumentos. Por exemplo:

  1. chamando o método direitos(), apresentado na Listagem 1, para mostrar os direitos autorais da aplicação:
    direitos();
  2. chamando o método tabuada(), apresentado na Listagem 1, para montar a tabuada do número 7 (sete):
    tabuada(7);
  3. chamando o método IMC() para calcular o índice de massa corporal de uma pessoa com peso corporal de 82,500 quilogramas e altura de 1,77metros:
    System.out.printf("IMC= %.2f", IMC(82.500, 1.77));
  4. chamando o método GEB() para calcular o gasto energético basal em quilocalorias (kcal) de uma pessoa do sexo masculino, peso corporal de 82,500 quilogramas, altura de 177 centímetros e 46 anos de idade:
    System.out.printf("GEB= %.2f kcal", GEB('M', 82.500, 177, 46));

Um método é uma parte separada do código de uma classe e somente é executado quando o seu nome (ou identificador) for referenciado em uma sentença de chamada de método. Quando um método é chamado para execução, é realizada a associação e passagem de parâmetros obedecendo a ordem de declaração: o 1º argumento é associado ao 1º parâmetro; o 2º argumento é associado ao 2º parâmetro e assim por diante. Portanto, no momento de uma chamada é importante que a quantidade de argumentos na chamada deva ser igual à quantidade de parâmetros do método. E ainda, o tipo do argumento enviado deve ser compatível com o tipo do respectivo parâmetro de entrada.

A Listagem 2 apresenta uma classe Java que implementa três versões para a tarefa de mostrar no dispositivo de saída padrão uma mensagem um determinado número de vezes. Essas versões apresentam diferenças alcançadas, principalmente, pela quantidade de parâmetros usados na chamada do respectivo método.

Listagem 2. Comunicação entre métodos através de parâmetros.


public class Exemplo2 {

  public static void main(String[] args) {
    mensagemUm();
    System.out.printf("\n");
    mensagemDois(5);
    System.out.printf("\n");
    mensagemTres(5, "MÉTODOS EM JAVA");
    System.out.printf("\n");
    mensagemTres(4, "PATO BRANCO/PR");
  }

  public static void mensagemUm() {
    int i;
    for (i=1; i<=3; i++) {
      System.out.printf("%d- DevMedia 
      | Canal Java\n", i);
    }
  }

  public static void mensagemDois(int n) {
    int i;
    for (i=1; i<=n; i++) {
      System.out.printf("%d- DevMedia 
      | Canal Java\n", i);
    }
  }

  public static void mensagemTres(int n, String s) {
    int i;
    for (i=1; i<=n; i++) {
      System.out.printf("%d- %s\n", i, s);
    }
  }

}

No método main() da aplicação Java apresentada na Listagem 2, foram realizadas as chamadas aos métodos mensagemUm(), mensagemDois() e mensagemTres(). O método mensagemUm() não possui parâmetros, e foi implementado usando um processo de repetição (for)para mostrar 3 (três) vezes a mensagem “DevMedia | Canal Java” no dispositivo de saída. Nesse método o número de vezes e a mensagem são valores constantes e,portanto, o resultado da execução do método será sempre o mesmo. Já o método mensagemDois(), implementado com um parâmetro inteiro identificado por n, permite que o resultado alcançando seja um pouco mais flexível. A flexibilidade no resultado ocorre porque o número de vezes que a mensagem “DevMedia | Canal Java” será exibida depende do valor do argumento enviado para o parâmetro n na respectiva chamada do método. Por último, o método mensagemTres(), implementado com um parâmetro int identificado por n e um parâmetro String identificado por s, conquista uma flexibilidade total no resultado. Isso ocorre porque o número de vezes(parâmetro n) que a mensagem será exibida e a mensagem (parâmetro s) serão definidos somente no momento que a chamada do método for realizada. Por exemplo:

  1. definindo na chamada do método mensagemTres() que a mensagem “MÉTODOS EM JAVA” será mostrada 5 (cinco) vezes:
    mensagemTres(5, "MÉTODOS EM JAVA");
  2. definindo na chamada do método mensagemTres() que a mensagem “PATO BRANCO/PR” será mostrada 4 (quatro) vezes:
    mensagemTres(4, "PATO BRANCO/PR");

Na Figura 5 pode-se observar a execução da classe Exemplo2, desenvolvida para demonstrar que os parâmetros funcionam como canais de comunicação entre os métodos de uma aplicação Java.

Executando a classe Exemplo2 para demonstrar a finalidade dos parâmetros

Figura 5. Executando a classe Exemplo2 para demonstrar a finalidade dos parâmetros.

Os métodos mensagemUm(), mensagemDois() e mensagemTres() apresentados na Listagem 2, na realidade desempenham a mesma tarefa de mostrar uma mensagem um determinador número de vezes. Esses três métodos poderiam ser implementados, na mesma classe, aplicando o mecanismo da sobrecarga de método (overloading), que permite que um método possa apresentar diversos tratamentos diferentes de acordo com os parâmetros usados na chamada. Na prática têm-se dois ou mais métodos com o mesmo nome, mas aceitando parâmetros diferentes (métodos com assinaturas diferentes). A sobrecarga de método será tema abordado nos próximos artigos sobre métodos em Java.

Promoção de argumentos

Um recurso importante das chamadas de método é a promoção de argumentos. O Java promoverá um argumento de chamada de método a fim de coincidir com seu respectivo parâmetro de acordo com as regras de promoção. Por exemplo, um programa pode chamar o método sqrt(), square root ou raiz quadrada, da classe Math, com um argumento inteiro mesmo que o método espere receber um argumento double. Por exemplo, a instrução de chamada:

System.out.printf(“Raiz
quadrada de 4 é igual a %.2f\n”, Math.sqrt(4));

será avaliada corretamente para mostrar o resultado 2.00. Essa situação ocorre porque o parâmetro da declaração do método faz com que o Java converta o valor 4 do tipo int no valor 4.0 do tipo double antes de enviar o valor para o método sqrt().

Tentar essas conversões pode levar a erros de compilação se as regras de promoção do Java não forem satisfeitas. A Tabela 1 mostra as regras de promoção indicando quais conversões são autorizadas.

Promoções válidas de argumentos

Tabela 1. Promoções válidas de argumentos – Fonte: DEITEL (2005, pág. 173).

Métodos public static

Segundo DEITEL (2005, pág. 166), cada classe fornece métodos que realizam tarefas sobre os objetos da classe. Por exemplo, a partir do Java 1.5 ou Java 5.0, o pacote de classes java.util disponibilizou a classe Scanner, que implementa operações de entrada de dados através do teclado. Para utilizá-la em uma aplicação, o seguinte conjunto de instruções deverá ser implementado:

// importando o pacote “java.util”
import java.util.Scanner;
// instancia e cria o objeto “teclado” a partir da classe “Scanner” para realizar a entrada de
dados através do teclado (System.in)
Scanner teclado = new Scanner(System.in);
int n;
System.out.printf("Informe um número para a tabuada:\n");
// aceita a entrada de um número inteiro chamando o método “nextInt()” da classe “Scanner”
no objeto “teclado”
n = teclado.nextInt();

Novamente em DEITEL (2005, pág. 167), tem-se que embora a maioria dos métodos seja executada em resposta a chamadas de métodos em objetos específicos, esse nem sempre é o caso. Às vezes um método realiza uma tarefa que não depende do conteúdo de nenhum objeto. Esse método se aplica à classe como um todo e é conhecido como método static (estático) ou método de classe. É comum encontrar classes que disponibilizam grupos de métodos static implementados para realizar tarefas de propósitos gerais. Por exemplo, um programa pode chamar o método de classe pow(), power ou potência, da classe Math, para calcular o valor de 2 elevado à potência de 8 (isto é, 28) usando a seguinte instrução:

System.out.printf(“2 elevado a 8 é igual a %.0f\n”, Math.pow(2, 8));

A classe Math, além dos métodos sqrt() e pow() citados nos exemplos anteriores, fornece uma coleção de métodos static que permite realizar cálculos matemáticos comuns; entre eles destacam-se: abs(x) – valor absoluto de x; max(x, y) – maior valor de x e y; min(x, y) – menor valor de x e y; e, sin(x) – seno trigonométrico de x.

Para declarar métodos de classe deve-se utilizar apalavra-chave static antes do tipo de retorno no cabeçalho do método. Por exemplo, o método de classe soma() projetado para somar os valores dos parâmetros a e b,pode apresentar a seguinte implementação:

public static int soma(int a, int b) {
return(a +b);
}

A instrução de chamada de qualquer método static deve ser realizada especificando o nome da classe em que o método está implementado, seguido do operador ponto (.) e pelo nome do método, obedecendo à sintaxe:

NomeDaClasse.nomeDoMétodo([argumentos]);

O modificador de acesso public, também usado na declaração de um método static, indica que o método pode ser utilizado livremente (sem restrições) por outras classes do mesmo projeto. Por exemplo, a chamada do método soma() implementado na classe Exemplo (Figura 3) poderia ser realizada através da seguinte instrução:

System.out.printf("5+ 3 = %d\n\n", Exemplo.soma(5, 3));

A Listagem 3 apresenta uma classe Java que implementa chamadas de métodos estáticos.

Listagem 3. Chamando métodos static.


public class Exemplo3 {

  public static void main(String[] args) {
    System.out.printf("5 + 3 = %d\n\n", 
    Exemplo.soma(5, 3));
    
    Exemplo1.tabuada(9);

    Exemplo2.mensagemDois(3);
  }

}

No método main() da aplicação Java apresentada na Listagem 3, foram realizadas 3 (três) chamadas a métodos static:

  1. chamando o método soma() implementado na classe Exemplo (Figura 3);
  2. chamando o método tabuada() implementado na classe Exemplo1 (Listagem 1); e,
  3. chamando o método mensagemDois() implementado na classe Exemplo2 (Listagem 2).

Na Figura 6 pode-se observar a execução da classe E xemplo3, que realiza a chamada de métodos static ou métodos de classe.

Executando a classe Exemplo3na chamada de métodos static

Figura 6. Executando a classe Exemplo3na chamada de métodos static.

Conclusão

Neste primeiro artigo sobre métodos em Java foram apresentados os elementos chaves da programação estruturada que organiza a solução de problemas computacionais em pedaços. Como o Java é uma linguagem de programação orientada a objetos, inicialmente foi apresentada a sintaxe usada na definição de uma classe. A classe em Java corresponde ao principal bloco de construção de sistemas de software.

Na sequência, os métodos foram definidos como serviços implementados na forma de um conjunto de instruções da linguagem que realizam alguma tarefa específica e podem como resultado retornar um valor. A execução de um método ocorre quando um módulo envia uma mensagem com informações que instruem como o método deverá executar sua tarefa. Essas informações são conhecidas como parâmetros, que funcionam como canais de transferência de dados entre os métodos. Uma situação importante nas chamadas de método é a promoção de argumentos, que ocorre para converter o valor de um argumento no tipo que o método espera receber no seu parâmetro correspondente.

Na última parte do artigo foi realizado um estudo sobre os métodos estáticos ou métodos de classe, que são desenvolvidos para realizar tarefas de propósitos gerais e que não dependem do conteúdo de nenhum objeto na sua utilização.


Referências

  • DEITEL, H.M. (2005) Java: Como Programar. São Paulo: Person Prentice Hall, 6ª edição.
  • Capítulo 3- Introdução a classes e objetos, páginas 57-85.
  • Capítulo 6- Métodos: um exame mais profundo, páginas 164-202.

Links


Parte II - Veja abaixo a segunda parte do artigo - Agora as partes I e II foram compiladas em um único artigo. Bons estudos :)


Métodos: módulos de programa em Java – Parte 2

Do que se trata o artigo: Este artigo trata da organização da solução de problemas computacionais em módulos, da utilização das estruturas homogêneas de dados (vetores ou matrizes) como argumentos (parâmetros) em chamadas de métodos, da implementação do mecanismo recursivo e da sobrecarga de métodos.


Em que situação o tema é útil:
Na organização da solução de problemas computacionais em módulos (em Java, métodos). Na utilização dos vetores ou matrizes como argumentos (ou parâmetros) em chamadas de métodos. Na implementação da recursividade permitindo a um subprograma (função ou método) chamar a si mesmo. Na utilização do polimorfismo, aspecto importante das linguagens orientadas a objetos, que na sua implementação mais simples é oferecida pelo Java por meio da sobrecarga de métodos.

Métodos: módulos de programa em Java – Parte 2
Um módulo (em Java, método) pode ser definido como um conjunto de instruções da linguagem que realizam alguma tarefa (procedimento algorítmico) e que eventualmente pode retornar um valor em resposta a sua chamada. Um método precisa usar o comando return para definir o valor de retorno, podendo em alguns casos ser útil desenvolver a solução de partes do problema usando múltiplos comandos return. As variáveis (em Java, objetos) do tipo vetor ou matriz são utilizadas para enviar um conjunto de valores indexados em chamadas de métodos. A recursividade é uma habilidade que permite a um subprograma (função ou método) chamar a si mesmo. O polimorfismo (várias formas), possibilitando a sobrecarga de métodos (method overload), permite que um método possa apresentar diversos tratamentos diferentes de acordo com os parâmetros usados na sua chamada. É sobre tudo isso que abordaremos neste artigo.

À medida que os problemas computacionais vão se tornando maiores e mais complexos, sempre é possível simplificar a solução dividindo o programa em partes menores, chamadas de módulos. Segundo DEITEL (2005, pág. 165), há três tipos de módulos em Java: métodos, classes e pacotes. Os métodos, denominados funções ou procedimentos nas linguagens de programação estruturada, são serviços implementados na forma de um conjunto de instruções da linguagem (procedimentos algorítmicos) que realizam alguma tarefa específica e podem, como resultado, retornar um valor.

O principal objetivo da modularização é permitir gerenciar a complexidade no desenvolvimento de programas de grande porte. Com a subdivisão de programas complexos em módulos, algumas vantagens são conquistadas como a legibilidade (trechos de código mais simples), a manutenibilidade (favorece a detecção e correção de erros) e a produtividade (facilita a reutilização de software).

Na primeira parte do artigo sobre métodos, publicada na Edição 25 da revista easy Java Magazine, foram apresentadas as características da programação modular em Java, destacando como declarar e utilizar módulos para facilitar o projeto, implementação e manutenção de grandes programas.

Nesta segunda parte do artigo, serão apresentados através de dois estudos de casos (fatorial de um número e índice de massa corporal), como os métodos realizam cálculos e fornecem valores como respostas, através de comandos return, às respectivas chamadas. Também será discutido que em alguns casos é útil implementar a solução de problemas computacionais usando múltiplos comandos return. Na sequência, ainda serão apresentados: a) como utilizar os vetores e matrizes como argumentos (parâmetros) em chamadas de métodos; b) a recursividade, que é o mecanismo que permite a um subprograma (função ou método) chamar a si mesmo; e, c) o polimorfismo na implementação da sobrecarga de métodos.

Comando return

Os métodos, denominados funções ou procedimentos nas linguagens de programação estruturada, são trechos de código que realizam alguma tarefa específica e podem como resultado retornar um valor ao módulo chamador. Um método precisa usar o comando return para definir o valor de retorno, obedecendo à sintaxe:

return (expressãoDeRetorno);

O tipo do valor que o método retorna é determinado no cabeçalho do método, por exemplo:

  • Para indicar que o método não deverá retornar valor, deve-se usar a palavra reservada void, conforme o código:
    public static void mostrarValores(int n) { … }
  • Para indicar que o método deverá retornar um número inteiro (int), codificamos da seguinte forma:
    public static int
    fatorial(int n) { ... }

Segundo DEITEL (2005, pág. 219), há três maneiras de retornar o controle à instrução que chama um método:

  1. Se o método não retornar um valor, o controle retornará quando o fluxo de execução do programa alcançar a chave de fechamento (}) do corpo do método;
  2. Se o método não retornar um valor, o controle retornará quando a instrução return for executada;
  3. Se o método retornar um valor, a instrução return (expressão) avalia a expressão e então retorna o resultado ao módulo chamador.

Quando encontra um comando return, Java finaliza imediatamente a execução do método e transfere o fluxo de execução ao módulo chamador substituindo o trecho do comando que o invocou pelo resultado retornado.

Qualquer código implementado depois de uma instrução return no corpo de um método não consegue ser alcançado pelo fluxo de execução tornando-se inacessível. Situações como esta não são comuns e devem ser evitadas porque provocam erros em tempo de compilação do tipo “unreachable statement”.

Estudo de caso 1 – Fatorial de um número

Na matemática, o fatorial de um número natural positivo n, representado por n!, é o produto de todos os inteiros positivos começando em n e decrescendo até 1. Ou seja:

n! = n x (n - 1) x (n - 2) x (n - 3) x ... x 3 x 2 x 1

Segundo a definição, o fatorial de 6, representado por 6!, é igual a:

6! = 6 x 5 x 4 x 3 x 2 x 1 = 720

Na Listagem 1, pode-se observar uma classe Java que implementa o cálculo do fatorial de um número natural positivo.

Listagem 1. Calculando o fatorial de um número.


import java.util.Scanner;
  public class Exemplo1 {
    public static void main(String[] args) {
      Scanner ler = new Scanner(System.in);
      int n;
      System.out.printf("Informe um número positivo 
      para o cálculo do fatorial:\n");
      n = ler.nextInt();
      System.out.printf("\nResultado:\n");
      mostrarValores(n);
      System.out.printf("%d\n", fatorial(n));
    }
    public static void mostrarValores(int n) {
      System.out.printf("%d! = ", n);
      int i;
      for (i=n; i>=1; i--) {
        if (i != 1)
           System.out.printf("%d x ", i);
        else System.out.printf("%d = ", i);
      }
    }
    public static int fatorial(int n) {
      int i, f;
   
      f = 1;
      for (i=n; i>=1; i--) {
        f = f * i;
      }
      return(f);
    }
   
  }

No método main() dessa aplicação Java foi implementada a entrada de dados para ler um número inteiro positivo usado no cálculo do fatorial, através da sentença n = ler.nextInt(). Os resultados são definidos com as chamadas aos métodos mostrarValores() e fatorial(), realizadas nas sentenças mostrarValores(n) e System.out.printf("%d\n", fatorial(n)), respectivamente.

No corpo do método mostrarValores() da Listagem 1, através de uma operação de saída usando o método printf(), é exibido o número do fatorial (parâmetro n) seguido dos símbolos “! =”. A seguir, através de uma instrução de repetição (for) controlada pela variável i, é montada a sequência de números de n até 1 decrescendo de 1. A cada passo da repetição um número é mostrado juntamente com o símbolo “x”; exceção feita ao último passo, quando o valor será mostrado seguido pelo símbolo “=”.

O método fatorial() ainda da Listagem 1, calcula usando um processo de repetição (for) o produto de todos os inteiros começando pelo valor do parâmetro n e decrescendo até 1, definindo o resultado na variável f. No final do corpo de instruções do método fatorial() é retornado o fatorial de n através da sentença return (f).

Na Figura 1 pode-se observar a execução da classe Exemplo1, desenvolvida para realizar o cálculo do fatorial de um número natural positivo.

Executando a classe Exemplo1 no cálculo do fatorial de um número

Figura 1. Executando a classe Exemplo1 no cálculo do fatorial de um número.

Estudo de caso 2 – Índice de massa corporal

O Índice de Massa Corporal (IMC), que é uma medida utilizada pela Organização Mundial de Saúde para avaliar o grau de obesidade de um indivíduo, é calculado pela relação entre o peso (em kg) dividido pelo quadrado da altura (em metros), como mostra o exemplo da Figura 2.

Exemplo de cálculo do índice de massa corporal

Figura 2. Exemplo de cálculo do índice de massa corporal.

Uma vez calculado, o IMC poderá ser classificado segundo uma tabela fornecida pelo Sistema de Vigilância Alimentar e Nutricional (SISVAN). Levando em consideração indivíduos adultos, têm-se as seguintes classificações:

  1. Valores de IMC abaixo de 18,5: adulto com baixo peso;
  2. Valores de IMC maior ou igual a 18,5 e menor que 25,0: adulto com peso adequado;
  3. Valores de IMC maior ou igual a 25,0 e menor que 30,0: adulto com sobrepeso;
  4. Valores de IMC maior ou igual a 30,0: adulto com obesidade.

A Listagem 2 apresenta uma classe Java que implementa o cálculo e a interpretação do índice de massa corporal de um indivíduo adulto.

Listagem 2. Calculando o índice de massa corporal.


import java.util.Scanner;
  public class Exemplo2 {
    public static void main(String[] args) {
      Scanner ler = new Scanner(System.in);
      double pc, alt, vlrIMC;
      System.out.printf("Informe o peso corporal (em kg):\n");
      pc = ler.nextDouble();
      System.out.printf("\nInforme a altura (em metros: 1,77 por exemplo):\n");
      alt = ler.nextDouble();
      vlrIMC = IMC(pc, alt);
      System.out.printf("\nResultados...:\n");
      System.out.printf("IMC..........: %.13f\n", vlrIMC);
      System.out.printf("Classificação: %s\n", interpretaIMC(vlrIMC));
    }
     public static double IMC(double pc, double alt) {
      return(pc / (alt * alt));
    }
    public static String interpretaIMC(double vlrIMC) {
      if (vlrIMC < 18.5)
         return("baixo peso");
      else if (vlrIMC < 25.0)
              return("peso adequado");
           else if (vlrIMC < 30.0)
                   return("sobrepeso");
                else return("obesidade");
    }
  }

No método main() dessa aplicação Java foram implementadas as entradas dos dados usados no cálculo do IMC, peso corporal e altura de um indivíduo, através das sentenças pc = ler.nextDouble() e alt = ler.nextDouble(), respectivamente. Os resultados são definidos com as chamadas aos métodos IMC() e interpretaIMC() realizadas nas sentenças vlrIMC = IMC(pc, alt) e System.out.printf("Interpretação: %s\n", interpretaIMC(vlrIMC)), respectivamente.

Na Listagem 2, o método IMC() calcula e retorna o índice de massa corporal fazendo a relação entre o peso corporal (parâmetro pc) dividido pelo quadrado da altura (parâmetro alt). Já o método interpretaIMC() recebe através do parâmetro vlrIMC o IMC calculado e através de uma estrutura condicional (if-else-if) retorna a classificação de acordo com os valores fornecidos pela tabela SISVAN.

Na Figura 3 pode-se observar a execução da classe Exemplo2, desenvolvida para calcular e interpretar o índice de massa corporal (IMC) de um indivíduo adulto.

Executando a classe Exemplo2 no cálculo do índice de massa corporal

Figura 3. Executando a classe Exemplo2 no cálculo do índice de massa corporal.

Múltiplos comandos return

Normalmente, como parte da solução de problemas computacionais, os métodos realizarão procedimentos algorítmicos como cálculos e fornecerão como resposta, através de um comando return, um valor ao módulo chamador. Porém, em alguns casos pode ser útil implementar a solução contendo múltiplos comandos return, um em cada caminho de uma estrutura condicional (if-else-if), por exemplo.

O método sinal() apresentado na Listagem 3 implementa uma solução baseada em múltiplos comandos return para avaliar a relação entre dois valores definindo o valor de retorno através de uma estrutura de comandos condicionais. Como os múltiplos comandos return estão implementados em caminhos diferentes do conjunto de comandos if, um e somente um comando será executado.

Listagem 3. Método usando múltiplos comandos return.


public class Exemplo3 {
    public static void main(String[] args) {
      int i, a, b;
      for (i=1; i<=10; i++) {
        a = (int)Math.round(Math.random() * 9);
        b = (int)Math.round(Math.random() * 9);
        System.out.printf("%2do. par de valores { %d %c %d }\n",
          i, a, sinal(a, b), b);
      }
    }
    // solução baseada na utilização de múltiplos comandos "return"
    public static char sinal(int a, int b) {
      if (a < b)
         return('<');
      else if (a == b)
              return('=');
           else return('>');
    }
  }

No método main() dessa aplicação Java foram declaradas as variáveis: i, utilizada para controlar a instrução de repetição; a e b, usadas para receber números aleatórios. Na instrução de repetição for, serão gerados 10 (dez) pares de números inteiros aleatórios no intervalo de 0 a 9, definidos pela expressão (int)Math.round(Math.random() * 9), que serão enviados e comparados no método sinal() para definir a relação existente entre eles. O método sinal() compara os dois valores inteiros recebidos através dos parâmetros a e b e retorna como resultado, o caractere ‘<’, se o primeiro valor for menor que o segundo; o caractere ‘=’, se os valores forem iguais; e, o caractere ‘>’, se o primeiro valor for maior que o segundo. Neste caso, são utilizados três comandos return, cada qual retornando um valor caractere que representa a condição específica.

Na Figura 4 pode-se observar a execução da classe Exemplo3, que define o valor da resposta de uma chamada de método através de múltiplos comandos return.

Figura 4. Executando a classe Exemplo3 na chamada de um método com múltiplos comandos return.

Uma nova versão para o método sinal(), usando apenas um comando return, pode ser observada na Listagem 4. Nesta implementação o valor de retorno é mantido na variável result e um único caminho de retorno é definido no final do corpo de instruções do método através da sentença return (result).

método sinal, usando apenas um comando return

Listagem 4. Abordagem usando apenas um comando return.

public static char sinal(int a, int b) {
    char result;
    if (a < b)
       result = '<';
    else if (a == b)
            result = '=';
         else result = '>';
    return(result);
  }

A escolha entre utilizar somente um ou múltiplos comandos return deve ser tomada para tornar o código do método o mais legível e facilmente modificável quanto possível. Uma boa prática de programação sugere que na implementação da solução para métodos simples, deve-se usar a abordagem com múltiplos comandos return e para métodos complexos, implementá-la usando somente um comando return.

Passando vetores e matrizes para métodos

Segundo DEITEL (2005, pág. 219), as linguagens de programação oferecem duas maneiras de passar argumentos em chamadas de método: passagem por valor e passagem por referência. Quando um argumento é passado por valor, uma cópia do valor do argumento é passada para o método chamado que funciona exclusivamente com a cópia. As alterações realizadas na cópia dentro do método chamado não afetam o valor da variável original no chamador. Já quando um argumento é passado por referência, o método chamado pode acessar o valor do argumento no chamador diretamente e modificar esses dados, se necessário.

O Java, ao contrário de algumas outras linguagens, não permite que o programador escolha passar por valor ou passar por referência. Todos os argumentos são passados por valor. No entanto, o mecanismo de passagem de argumentos permite que a chamada de método passe dois tipos de valores para um método:

  1. As cópias de valores primitivos (por exemplo, valores do tipo int e double);
  2. As cópias de referências para objetos (inclusive referências a vetores e matrizes).

No primeiro caso, quando um método modifica um parâmetro do tipo primitivo, as alterações não produzem nenhum efeito no valor original do argumento no método chamador. No caso de argumentos do tipo vetor ou matriz, quando a referência do objeto correspondente é passada por valor, um método mesmo assim pode interagir diretamente com o objeto, uma vez que o parâmetro no método chamado e o argumento no método chamador referenciam o mesmo objeto na memória (produzindo assim o mesmo efeito da passagem de parâmetro por referência).

Para passar um argumento do tipo vetor ou matriz para um método, deve-se especificar o nome do vetor ou matriz sem nenhum colchete. Por exemplo, se as variáveis forem declaradas e inicializadas como:

int vetorA[] = {1, 2, 3, 4, 5}; // declaração e inicialização do ‘vetorA’ com 5 elementos: 1, 2, 3, 4 e 5
  int vetorB[] = {3, 2, 1}; // declaração e inicialização do ‘vetorB’ com 3 elementos: 3, 2 e 1
  int matrizA[][] = { {1, 2, 3, 4}, {5, 6, 7, 8} }; // declaração e inicialização da ‘matrizA’ com duas linhas:
  // 1ª linha com os elementos: 1, 2, 3 e 4
  // 2ª linha com os elementos: 5, 6, 7 e 8
  int matrizB[][] = { {1}, {2, 3, 4}, {5, 6, 7, 8, 9} }; // declaração e inicialização da ‘matrizB’ com três linhas:
  // 1ª linha com o elemento: 1
  // 2ª linha com os elementos: 2, 3 e 4
  // 3ª linha com os elementos: 5, 6, 7, 8 e 9
  então as chamadas de métodos:
   mostrarVetor(“1º Vetor”, vetorA); 
   mostrarVetor(“2º Vetor”, vetorB); 
   mostrarMatriz(“1ª Matriz”, matrizA); 
   mostrarMatriz(“2ª Matriz”, matrizB);

Passam as referências dos vetores vetorA e vetorB, para o método mostrarVetor() e das matrizes matrizA e matrizB, para o método mostrarMatriz(). Como todo objeto array (vetor ou matriz) conhece seu próprio comprimento através do campo length, não é necessário passá-lo como um argumento adicional na chamada.

Em contrapartida, para que um método possa receber a referência de um vetor ou matriz, na sua lista de parâmetros deve-se especificar um parâmetro do tipo array (identificador acompanhado dos colchetes). Por exemplo, os cabeçalhos dos métodos mostrarVetor() e mostrarMatriz() poderiam ser escritos como:

public static void mostrarVetor(String s, int v[]) {...}
  public static void mostrarMatriz(String s, int m[][]) {...}

Indicando que o método mostrarVetor() recebe a referência de um vetor de inteiros no parâmetro v e que o método mostrarMatriz() recebe a referência de uma matriz de inteiros no parâmetro m.

A Listagem 5 apresenta uma classe que demonstra a implementação da utilização de vetores e matrizes como argumentos para métodos.

Listagem 5. Passando vetores e matrizes para métodos.


public class Exemplo4 {
    public static void main(String[] args) {
      int vetorA[] = {1, 2, 3, 4, 5};
      int vetorB[] = {3, 2, 1};
      int matrizA[][] = { {1, 2, 3, 4}, {5, 6, 7, 8} };
      int matrizB[][] = { {1}, {2, 3, 4}, {5, 6, 7, 8, 9} };
      // passa a referência da variável 'vetorA'
      mostrarVetor("1º Vetor", vetorA); 
      System.out.println();
      // passa a referência da variável 'vetorB'
      mostrarVetor("2º Vetor", vetorB); 
      System.out.println();
      // passa a referência da variável 'matrizA'
      mostrarMatriz("1ª Matriz", matrizA); 
      System.out.println();
      // passa a referência da variável 'matrizB'
      mostrarMatriz("2ª Matriz", matrizB);
    }
     public static void mostrarVetor(String s, int v[]) {
      int i, n;
       System.out.printf("%s:\n", s);
       n = v.length; // determina o tamanho do vetor
      for(i=0; i<n; i++) {
        System.out.printf("%do. elemento = %d\n", (i+1), v[i]);
      }
    }
     public static void mostrarMatriz(String s, int m[][]) {
      int i, j, nl, nc;
       System.out.printf("%s:\n", s);
       nl = m.length; // determina o número de linhas da matriz
      for (i=0; i<nl; i++) {
        System.out.printf("%da. linha: ", (i+1));
        nc = m[i].length; // determina o número de colunas da i-ésima linha
        for (j=0; j<nc; j++) {
          System.out.printf("%d ", m[i][j]);
        }
        System.out.printf("\n");
      }
    }
   }

No método main() dessa aplicação Java foram declarados e inicializados dois vetores, vetorA com 5 (cinco) e vetorB com 3 (três) elementos cada, e duas matrizes, matrizA de ordem 2 x 4 (duas linhas por quatro colunas) e matrizB com 3 (três) linhas de tamanhos diferentes (1ª linha com uma coluna, 2ª linha com três colunas e a 3ª linha com cinco colunas). A seguir, foram realizadas as chamadas aos métodos mostrarVetor(), para mostrar os elementos dos vetores e mostrarMatriz(), para mostrar os elementos das matrizes.

Na implementação do método mostrarVetor() da Listagem 5, foi utilizado o campo length para acessar o tamanho do vetor v, que recebeu as referências dos vetores vetorA e vetorB nas respectivas chamadas no método main(). Através de uma instrução de repetição (for), todos os elementos do vetor são mostrados.

Ainda na Listagem 5, agora no método mostrarMatriz(), foi utilizado o campo length da matriz m, que recebeu as referências das matrizes matrizA e matrizB nas respectivas chamadas no método main(), em dois momentos:

  1. Para acessar o número de linhas (variável nl) da matriz na instrução nl = m.length;
  2. Para acessar o número de colunas (variável nc) da i-ésima linha na instrução nc = m[i].length.

Complementando o método, foram utilizadas duas instruções de repetição (for) controladas pelas variáveis i e j para percorrer e mostrar, linha por linha, os elementos da matriz.

Na Figura 5 pode-se observar a execução da classe Exemplo4, desenvolvida para exemplificar a passagem de vetores e matrizes para métodos.

Executando a classe Exemplo4 na passagem de vetores e matrizes para métodos

Figura 5. Executando a classe Exemplo4 na passagem de vetores e matrizes para métodos.

O tema referente à passagem de vetores e matrizes para métodos também foi abordado nos artigos “Vetores – parte 2”, publicado na Edição 19 da revista easy Java Magazine e “Matrizes”, publicado na Edição 22, também da easy Java Magazine.

Recursividade

Alguns problemas computacionais apresentam a seguinte propriedade: cada instância do problema contém uma instância menor do mesmo problema. A ideia básica na solução desse tipo de problema consiste em diminuir sucessivamente o problema em uma instância menor ou mais simples, até que o tamanho ou a simplicidade do problema reduzido permita resolvê-lo de forma direta, sem recorrer a si mesmo. Quando isso ocorre, diz-se que o algoritmo atingiu uma condição de parada, a qual deve estar presente em pelo menos um local no corpo do algoritmo. A aplicação desse processo produz um algoritmo recursivo.

Na programação de computadores, a recursividade é um mecanismo que permite a um subprograma (função ou método) chamar a si mesmo direta ou indiretamente. A implementação de métodos recursivos apresentam duas características:

  1. Condição de parada: resolvido sem utilização de recursividade, sendo este ponto geralmente um limite superior ou inferior da regra geral;

  2. Regra geral: reduz a solução do problema através da invocação recursiva de casos menores do mesmo problema. As novas instâncias do problema inicial são resolvidas com casos ainda menores, e assim sucessivamente, até atingir a condição de parada que finaliza o método.

Exemplos de problemas recursivos existem em grande número. Um clássico da recursividade é mostrado na função que calcula o fatorial de um número, apresentada na Figura 6, que tem as seguintes características:

  1. Condição de parada: fatorial de 0 (zero) é igual a 1 (um).
  2. Regra geral: fatorial de n é definido de forma recursiva pela expressão n x (n - 1)!.
Função fatorial recursiva

Figura 6. Função fatorial recursiva.

Na Listagem 6, pode-se observar uma classe Java que implementa o cálculo do fatorial de um número natural positivo de forma recursiva.

Listagem 6. Implementando a função fatorial recursiva.


import java.util.Scanner;
  public class Exemplo5 {
    public static void main(String[] args) {
      Scanner ler = new Scanner(System.in);
      int n;
      System.out.printf("Informe um número positivo 
      para o cálculo do fatorial:\n");
      n = ler.nextInt();
      System.out.printf("\n%d! = %d\n", n, fatorial(n));
    }
    public static int fatorial(int n) {
      if (n == 0)
         return(1); // condição de parada
      else return(n * fatorial(n - 1)); // chamada recursiva
    }
  }

No método main() dessa aplicação Java foi implementada a entrada de dados para ler um número inteiro positivo usado no cálculo do fatorial, através da sentença n = ler.nextInt(). O resultado é definido com a chamada ao método fatorial() realizada na sentença System.out.printf("\n%d! = %d\n", n, fatorial(n)).

No procedimento algorítmico do método fatorial() da Listagem 6, enquanto o valor do parâmetro n não for igual à zero, o método chama a si mesmo na sentença return (n * fatorial(n – 1)). É importante observar que a cada nova chamada recursiva o valor do argumento é definido pela expressão (n – 1), atendendo assim a regra geral da função fatorial. A condição (n == 0) é critério de parada das chamadas recursivas para este método.

Na Figura 7 pode-se observar a execução da classe Exemplo5, desenvolvida para realizar o cálculo do fatorial de um número natural positivo de forma recursiva.

Executando a classe Exemplo5 na chamada da função fatorial recursiva

Figura 7. Executando a classe Exemplo5 na chamada da função fatorial recursiva.

Para cada chamada de um método, recursivo ou não, é criado um “registro de ativação” na pilha de execução do programa. No registro de ativação são armazenados os parâmetros e as variáveis locais do método chamado, bem como o endereço de retorno ao módulo chamador. Ao final da execução do método, o registro é desempilhado e o fluxo de execução é devolvido ao ponto que ocorreu a chamada, como pode ser observado na Figura 8.

Pilha de chamadas do método fatorial recursivo

Figura 8. Pilha de chamadas do método fatorial recursivo.

Há certos algoritmos que são mais eficientes quando implementados de maneira recursiva, mas a recursividade é algo que deve ser evitado sempre que possível. Analisando que na pilha de execução são armazenados os parâmetros e as variáveis locais do método chamado e que a cada nova chamada é criado uma cópia destas variáveis na pilha. Soluções recursivas tendem a consumir muita memória e consequentemente apresentar uma solução mais lenta.

Sobrecarga de métodos

O polimorfismo, palavra originária do Grego que significa várias (poli) formas (fismo), é um dos aspectos mais importantes das linguagens orientadas a objetos. A sua implementação mais simples, oferecida pela linguagem Java, é por meio da sobrecarga de métodos (method overload).

A sobrecarga de métodos permite que um método possa apresentar diversos tratamentos diferentes de acordo com os parâmetros usados na chamada. Na prática, têm-se dois ou mais métodos com o mesmo nome, mas aceitando parâmetros diferentes. Para que estes métodos homônimos possam ser distinguidos, eles devem ter uma assinatura diferente. A assinatura de um método é definida pelo nome do método e complementada pela lista que indica o nome e o tipo de todos os seus parâmetros formais; sendo assim, métodos com o mesmo nome são considerados diferentes quando recebem um diferente número ou diferentes tipos de argumentos, e têm, portanto, assinaturas diferentes. O valor de retorno não faz parte da assinatura, pois se admite a conversão automática de tipos, o que impede o compilador de identificar o método adequado.

A Listagem 7 apresenta uma classe Java que implementa três versões para a tarefa de mostrar no dispositivo de saída padrão uma mensagem um determinado número de vezes. Essas versões apresentam diferenças alcançadas, principalmente, pela quantidade de parâmetros usados na chamada do respectivo método.

Listagem 7. Implementando a sobrecarga de métodos.


public class Exemplo6 {
    public static void main(String[] args) {
      mensagem();
      System.out.printf("\n");
      mensagem(5);
      System.out.printf("\n");
      mensagem(5, "MÉTODOS EM JAVA");
      System.out.printf("\n");
      mensagem(4, "PATO BRANCO/PR");
    }
    public static void mensagem() {
      System.out.printf("1ª versão: método \"mensagem\"\n");
      int i;
      for (i=1; i<=3; i++) {
        System.out.printf("%d- DevMedia | Canal Java\n", i);
      }
    }
     public static void mensagem(int n) {
      System.out.printf("2ª versão: método \"mensagem\"\n");
       int i;
      for (i=1; i<=n; i++) {
        System.out.printf("%d- DevMedia | Canal Java\n", i);
      }
    }
     public static void mensagem(int n, String s) {
      System.out.printf("3ª versão: método \"mensagem\"\n");
       int i;
      for (i=1; i<=n; i++) {
        System.out.printf("%d- %s\n", i, s);
      }
    }
   }

No método main() dessa aplicação Java foram realizadas chamadas as 3 (três) versões do método mensagem(). Na primeira versão o método mensagem() não possui parâmetros, e foi implementado usando um processo de repetição (for) para mostrar três vezes a mensagem “DevMedia | Canal Java” no dispositivo de saída. Nesse método o número de vezes e a mensagem são valores constantes e, portanto, o resultado da execução do método será sempre o mesmo. Já a segunda versão do método mensagem(), implementado com um parâmetro inteiro identificado por n, permite que o resultado alcançando seja um pouco mais flexível. A flexibilidade no resultado ocorre porque o número de vezes que a mensagem “DevMedia | Canal Java” será exibida depende do valor do argumento enviado para o parâmetro n na respectiva chamada do método. Por último, a terceira versão do método mensagem(), implementado com um parâmetro int identificado por n e um parâmetro String identificado por s, conquista uma flexibilidade total no resultado. Isso ocorre porque o número de vezes (parâmetro n) e a mensagem (parâmetro s) que será exibida serão definidos somente no momento que a chamada do método for realizada. Por exemplo:

  1. Definindo na chamada que a mensagem “MÉTODOS EM JAVA” será mostrada 5 (cinco) vezes:
    mensagem(5, "MÉTODOS EM JAVA");
  2. Definindo na chamada que a mensagem “PATO BRANCO/PR” será mostrada 4 (quatro) vezes:
    mensagem(4, "PATO BRANCO/PR");

Na Figura 9 pode-se observar a execução da classe Exemplo6, desenvolvida com sobrecarga de métodos para permitir que o método mensagem() apresente formas de execução diferentes de acordo com os parâmetros usados na chamada.

Executando a classe Exemplo6 para demonstrar a sobrecarga de métodos

Figura 9. Executando a classe Exemplo6 para demonstrar a sobrecarga de métodos.

A API (Application Programming Interface) do Java utiliza intensivamente o mecanismo de sobrecarga de métodos, por exemplo, a classe java.lang.String, na qual o método indexOf() tem várias implementações. Esse método se destina a localizar a posição (ou índice) de um caractere ou substring dentro de uma cadeia de caracteres contida em objetos Strings. Veja as diferentes possibilidades para seu uso:

  • indexOf(char ch) – retorna a posição, dentro da String, da primeira ocorrência do caractere (ch) especificado;
  • indexOf(char ch, int n) – retorna a posição, dentro da String, da primeira ocorrência do caractere (ch) especificado a partir da posição (n) dada;
  • indexOf(String s) – retorna a posição, dentro da String, da primeira ocorrência da substring (s) especificada;
  • indexOf(String s, int n) – retorna a posição, dentro da String, da primeira ocorrência da substring (s) especificada a partir da posição (n) dada.

É transparente para o usuário de uma classe a existência de métodos sobrecarregados, permitindo a escolha da alternativa mais adequada. Ao utilizar uma classe na qual são implementados vários métodos sobrecarregados, fica a cargo do compilador verificar se existe um método apropriado em função da lista de argumentos indicada nas chamadas. Durante a execução, essa checagem é realizada novamente para selecionar e acionar o método apropriado, o que é chamado de ligação tardia (late binding). Quando o método a ser invocado é definido durante a compilação do programa, o mecanismo de ligação prematura (early binding) é utilizado.

Para a utilização de polimorfismo, a linguagem de programação orientada a objetos deve suportar o conceito de ligação tardia, onde a definição do método que será efetivamente invocado só ocorre durante a execução do programa. O mecanismo de ligação tardia também é conhecido pelos termos dynamic binding (ligação dinâmica) ou run-time binding (ligação em tempo de execução).

Conclusões

Neste segundo artigo sobre métodos foram apresentados exemplos de aplicações Java que organizam a solução de problemas computacionais em pedaços. No primeiro estudo de caso foi apresentada uma classe que implementa o cálculo do fatorial de um número natural positivo. Já no segundo estudo de caso a classe desenvolvida apresenta um conjunto de métodos utilizados no cálculo e na interpretação do índice de massa corporal de um indivíduo adulto.

Na sequência foi discutido que normalmente os métodos realizam cálculos e fornecem um valor como resposta, através de um comando return, ao módulo que efetuou a chamada. Também foi destacado que em alguns casos pode ser útil implementar a solução do problema contendo múltiplos comandos return, um em cada caminho de uma estrutura de comandos condicionais (if-else-if), por exemplo.

Outro aspecto da modularização estudado tratou da utilização dos vetores ou matrizes como argumentos (parâmetros) em chamadas de métodos. Quando um vetor ou matriz é utilizado em uma chamada de método, é enviada uma cópia da referência do argumento para o parâmetro no método chamado. Este mecanismo permite que o método chamado possa manipular o objeto do chamador diretamente, uma vez que ambos compartilham o mesmo espaço de memória.

A recursividade foi o mecanismo apresentado como alternativa para a solução de problemas computacionais em que cada instância do problema contém uma instância menor do mesmo problema. Na programação de computadores, a recursividade é uma habilidade que permite a um subprograma (função ou método) chamar a si mesmo.

Na última parte do artigo foi realizado um estudo sobre o polimorfismo na implementação da sobrecarga de métodos que permite que um método possa apresentar diversos tratamentos diferentes de acordo com os parâmetros usados na chamada. Na prática têm-se dois ou mais métodos com o mesmo nome, mas aceitando parâmetros diferentes (métodos com assinaturas diferentes).

Com este segundo artigo foi concluída a abordagem sobre métodos como módulos de programas em Java que são utilizados para estruturar a solução de problemas computacionais em pedaços.


Referências

  • DEITEL, H.M. (2005) Java: Como Programar. São Paulo: Person Prentice Hall, 6ª edição.
  • Capítulo 3- Introdução a classes e objetos, páginas 57-85.
  • Capítulo 6- Métodos: um exame mais profundo, páginas 164-202.
  • Capítulo 7- Arrays, páginas 203-242.
  • Capítulo 15- Recursão, páginas 551-581.

Links