Os métodos estáticos funcionam como abstrações para trechos de programas. Podemos
pegar um trecho de programa e dar um nome significativo para ele. No
ponto que o programador deseja executar aquele trecho de programa ele invoca
ou chama aquele trecho. Veremos abaixo alguns dos conceitos envolvidos. A
possibilidade de poder abstrair de como implementar um trecho de programa e
poder dar um nome a este trecho é um mecanismo muito importante. Todas as
linguagens de alto-nível provêm alguma forma de abstração que segue essa
descrição. Esta abstração se apresenta com outros nomes: procedimento
(procedure), subrotina (subroutine), função (function) e método (method).
Os programas desta
disciplina, vistos até este ponto, apresentavam todo seu código fonte dentro de
uma unidade da Linguagem Java com o nome de main:
class program{
public static void main(String[] args){
/* codigo fonte */
}
}
Esta unidade é denominada método
(method). O método main() é um método especial pois é o método
que funciona como ponto de entrada do fluxo de controle nos programas Java.
Quando solicitamos a execução de um programa Java, o fluxo de controle será
passado ao primeiro comando do método main() (na verdade conform será
visto à frente trata-se do método com nome main e um parâmetro do tipo arranjo
de String!). É o sistema de execução Java quem invoca ou chama o método main().
Antes de podermos chamar ou
invocar um método é necessário que seja feita a definição do método. A
definição de um método envolve vários conceitos que serão ilustrados de forma
incremental. Vemos abaixo um método bastante simples (não define nenhum
parâmetro, não tem variáveis locais e não retorna nenhum valor):
class
programa1{
/* 1 */
public static void main(String[] args){
/* 2 */
imprime();
/* 3 chamada ou invocacao */
}
/* 4 */
static void
imprime(){
/* 5 definicao */
System.out.println("um metodo simples!");
/* 6 do */
}
/* 7 metodo */
}
No programa acima definimos
um método estático (um método que não depende de instâncias ou objetos)
com nome imprime(). Este método não retorna valor. Um método pode
retornar ou não um valor. Quando o método não retorna nenhum valor, como no
caso acima, devemos utilizar a palavra chave void no cabeçalho da
definição. Um método pode retornar um valor pertencente a um dos oito tipos
primitivos ou uma referência (endereço de um arranjo ou objeto) ou não retornar
nenhum valor e neste caso devemos utilizar a palavra chave void. A ordem
de definição dos métodos não é importante em Java. Podemos reescrever o
programa acima definindo o método imprime() antes do método main().
Em tempo de execução, ao
ser chamado ou invocado o método imprime(), o fluxo de controle desvia
para o primeiro comando do método. Quando é executado o último comando de um
método, o fluxo de controle volta para o ponto onde o método foi invocado.
Veremos à frente como que o sistema de execução prepara uma estrutura
denominada registro de ativação ou quadro de ativação (activation
frame) para controlar as atividades de chamada, execução e retorno de um
método.
Na compilação do programa
acima, o compilador gera código para uma chamada de método correspondente à
linha 3 e gera código para o corpo do método correspondente à linha 6. Além
disso gera código para que seja construído, de forma adequada, um registro de
ativação, quando o método imprime() for invocado, ou seja, o código de
correspondente a linha 3, entre outras coisas, salva no registro de ativação o
endereço de retorno. O endereço de retorno é o endereço para onde desvia o
fluxo de controle quando o método termina.
Na execução do programa
acima o fluxo de controle está inicialmente com o sistema Java e é criado um
registro de ativação para invocar o método main(). Após isso é feito o
desvio para o primeiro comando do método main(), linha 3. O código
correspondente à linha 3 é uma invocação do método imprime(). É
criado um registro de ativação com conteúdo adequado e é feito o desvio do
fluxo de controle para o primeiro comando do método imprime(), isto é, linha 6.
Após a execução da linha 6 o fluxo de controle encontra código que consulta o
registro de ativação e retorna o fluxo de controle para a linha 4. A linha 4
corresponde a um código que retorna o fluxo de controle para o sistema de
execução Java e termina a execução do programa. A seqüência de execução
(simplificada) do programa acima é: O sistema Java invoca o método main().
O primeiro comando do método main(), linha 3, cria um registro de ativação que,
dentre outras informações, guarda o endereço de retorno. O código relativo à
invocação do método imprime() desvia o fluxo de controle para o código
correspondente à linha 6. A linha 6 corresponde a uma invocação do método println().
Neste ponto é criado um registro de ativação para a execução do método println().
O controle é desviado para o código do método println(). Quando termina a
execução do método o controle retorna para o código correspondente à linha 7,
destruindo o registro de ativação criado para a invocação do println().
O código relativo à linha 7 consiste em recuperar o endereço de retorno contido
no registro de ativação correspondente à invocação do método imprime(). O endereço
de retorno corresponde ao código correspondente à linha 4 e neste ponto também
é destruído o registro de ativação correspondente à invocação de imprime().
Ao retornar para o sistema é destruído o registro de ativação correspondente à
invocação do método main().
Resumindo a seqüência de eventos relativa à execução do programa acima:
Quando o método não tem parâmetros, não retorna um valor, não tem
variáveis locais, o registro de ativação serve basicamente para guardar o
endereço de retorno do fluxo de controle. A seguir vamos introduzir novos
elementos nas definições. Observe o novo método imprime do programa abaixo:
class
programa2{
public static void
main(String[] args){
imprime("um metodo simples!");
}
static void imprime(String
s){
System.out.println(s);
}
}
O novo imprime() possui
agora um parâmetro do tipo String. Este método, para ser invocado, tem que
receber um parâmetro do tipo String. A seqüência do fluxo de execução é similar
ao programa anterior, a principal diferença é que o código correspondente à
invocação do método imprime(), antes de fazer a invocação irá atribuir o valor da referência para a
cadeia "um método simples" para o parâmetro s. Os parâmetros
funcionam de forma similar a uma variável e ficam disponíveis no registro de
ativação de um método. O programador de um método pode usar os parâmetros da
mesma forma que podem usar uma variável declarada localmente ao método. No caso
acima o valor do parâmetro é passado para a invocação do método println().
O método abaixo mostra um
exemplo de passagem de parâmetros um pouco mais geral. Considere a
definição:
static void m(int p1, long p2, double p3, int[] p4, String p5){
System.out.println("inteiro:"+p1);
System.out.println("long:"+p2);
System.out.println("double:"+p3);
System.out.print("arranjo de inteiros:");
for(int i=0; i<p4.length; i++) System.out.print(p4[i]+"
");
System.out.println();
System.out.println(p5);
}
Considere a invocação:
m(123, 1234L, 12.34, new int[]{1,2,3}, "ola!");
Antes do controle desviar
para o primeiro comando do método m, será feita a avaliação das expressões
correspondentes a cada parâmetro. Após ser feita a avaliação das expressões
será feita a atribuição dos valores resultantes para cada parâmetro que ficarão
disponíveis no registro de ativação que será construído para a invocação.
Observe que o número e o tipo dos parâmetros na definição do método devem
sempre ser compatível com o número e o tipo das expressões na invocação. Os
parâmetros na definição do método são conhecidos como parâmetros formais,
os valores ou expressões dos parâmetros na invocação do método são conhecidos
como parâmetros reais. A invocação de um método faz com que os valores
dos parâmetros reais sejam atribuídos ao
parâmetros formais. Cada invocação de um método dá origem a um registro de
ativação contendo os parâmetros formais inicializados com os valores obtidos na
avaliação das expressões correspondentes aos parâmetros reais. Se fizermos uma
atribuição a um parâmetro formal esta atribuição, no caso da linguagem Java,
não afeta o parâmetro real correspondente:
class tstmetodo{
public static void main(String[] args){
int pr=3;
System.out.println("passou parametro
real:"+pr);
m(pr);
System.out.println("parametro real:"+pr);
}
static void m(int pf){
System.out.println("m recebeu o
valor:"+pf);
pf=5;
System.out.println("m atribuiu "+pf+"
ao parametro formal");
}
}
O programa acima imprime:
passou parametro real:3
m recebeu o valor:3
m atribuiu 5 ao parametro formal
parametro real:3
O modelo de passagem de
parâmetro da linguagem Java é denominado passagem de parâmetro por valor.
Existem outros mecanismos de amarramento (binding) de parâmetros em
outras linguagens. Os parâmetros e as variáveis declaradas dentro de um método
só estão disponíveis no escopo deste método. Isto é similar à questão do escopo
de variáveis definidas no corpo de um comando para(for). Quando o registro de ativação é construído é reservado espaço
para parâmetros e variáveis locais, quando o resgistro de ativação é destruído
as variáveis locais e parâmetros deixam de existir.
A linguagem Java é
“fortemente tipada” isso significa que a sintaxe da linguagem é muito rigorosa
com relação aos tipos de variáveis, parâmetros, expressões etc. Considere que
desejassemos escrever um método que recebesse um arranjo e “zerasse” este
arranjo. Sem o uso de técnicas especiais da linguagem Java (que não serão
vistas nesta disciplina básica) temos que escrever um método para tipo de
arranjo. No programa abaixo foram escritos dois métodos. O programa define um
método para zerar arranjo de int e um outro método para zerar arranjo de
double:
class zerarArranjo{
static void zeraArranjoInt(int[]
x){
for(int i=0; i<x.length; i++)
x[i]=0;
}
static void
zeraArranjoDouble(double[] x){
for(int i=0; i<x.length; i++)
x[i]=0.0;
}
public static void main(String[]
arg){
int[] a={1,2,3,4};
double[] b={1.0, 2.0, 3.0, 4.0,
5.0};
zeraArranjoInt(a);
zeraArranjoDouble(b);
}
}
A assinatura de um
método consiste (1) no seu nome e (2) no número e tipo dos argumentos ou
parâmetros formais. Assim, podemos ter vários métodos com o mesmo nome, mas
cada um deles deve ter número e tipo de parâmetros distintos. O trecho de
programa abaixo consiste em uma definição válida de métodos (de objetivos
didáticos já que eles não têm corpo). Apesar do nome de todos os métodos ser o
mesmo (main) eles se distinguem em função do número e tipo dos parâmetros:
static void main(int p){}
static void main(char p){}
static void main(double p){}
static void main(String p){}
static void main(long p){}
static void main(boolean p){}
static void main(int p1, int p2){}
static void main(int p1, int p2,
int p3){}
Um método pode retornar um
valor. Para isso devemos colocar no cabeçalho de definição, ao invés da palavra
chave void, o tipo do valor que o método irá retornar. Neste caso a execuçao (não é o texto!) do método
usualmente irá terminar quando da execução de um comando da forma:
return
<expressão>;
A expressão especificada no comando de retornar (return) deve ser de
tipo compatível na definição do cabeçalho do método. O procedimento abaixo
recebe como parâmetro um caractere hexadecimal e retorna um valor inteiro
correspondente ao caractere:
static int charHex2int(char c){
switch(c){
case '0': case '1': case '2': case '3': case '4':
case '5': case '6': case '7': case '8': case '9':
return c-'0';
case 'A': case 'B': case 'C': case 'D': case 'E':
case 'F':
return c-'A'+10;
case 'a': case 'b': case 'c': case 'd': case 'e':
case 'f':
return c-'a'+10;
default: return -1;
}
}
Observe que as expressões nos comandos de retorno são de tipo int. No
local onde existe a <chamada do método>, após a excecução do método,
passa a ficar disponível o valor retornado pelo método. Até este ponto da
disciplina o método que tinhamos mais contato era o método main. O método main
por ser um método do tipo void não exige a presença de um comando return. Mas
podemos usar o comando return (sem estar seguido de expressão!) nos métodos do
tipo void:
Observe os métodos m1 e m2
abaixo:
static void m1(){return;}
static void m2(){}
No caso de m1 o retorno é
explícito; no caso de m2 o retorno ocorre pelo que é denominado “drop off”( o fluxo
de controle saiu do bloco!), podemos considerar que o compilador coloca uma
instrução de return ao final do bloco do método m2. A especificação da
linguagem Java prescreve: (1)Se um método é do tipo void então no bloco não
devem aparecer return com expressões (só pode aparecer return sem expressão).
(2) Se um método não é do tipo void então se aparecer algum return ele deve ser
do tipo return <expressão>.
Um método pode ter
variáveis locais da mesma forma que já estavamos empregando no caso do método main().
As variáveis declaradas dentro de um método são de acesso exclusivo daquele
método. Na linguagem Java não podemos definir um método dentro de outro método
(não existem definições aninhadas de métodos). Observe o programa abaixo:
class programa3{
public static void main(String[] args){
int i=3;
System.out.println("i de m():"+m());
System.out.println("i de main():"+i);
}
static int m(){
int i=4;
return i;
}
}
No programa acima, durante a execução de m(), o registro de
ativação de m() conterá uma variável local com o mesmo nome da variável local
do método main(). A definição de um método define um espaço de nomes.
Um método pode invocar a si
mesmo. Quando um método invoca a si mesmo dizemos que ele é recursivo. O
programa abaixo mostra um método para calcular o fatorial de um número em uma
versão recursiva e outra não recursiva:
class programa4{
public static void main(String[] args){
System.out.println(fatNaoRecursivo(5));
System.out.println(fatRecursivo(5));
}
static int fatNaoRecursivo(int n){
if(n<2) return 1;
int f=1;
for(int i=2; i<=n; i++) f*=i;
return f;
}
static int fatRecursivo(int n){
if(n<2) return 1;
return fatRecursivo(n-1)*n;
}
}
No programa acima vemos que
a versão não recursiva que implementa o cálculo do fatorial utiliza um comando
for para iterar as multiplicações necessárias. Para podermos entender a versão
recursiva do método temos que lembrar que existe um registro de ativação para
cada invocação do método. A tabela abaixo mostra algumas caracteristicas da
correlação entre registros de ativação e o parâmetro n:
|
n |
No. de registros |
valor |
|
0 |
1 |
1 |
|
1 |
1 |
1 |
|
2 |
2 |
2 |
|
3 |
3 |
6 |
Como regra geral é sempre
preferível utilizar a versão não recursiva pois as versões recursivas são, em
geral, muito pouco eficientes. Considere a série de Fibonacci: 0,1, 1, 2, 3, 5,
8, 13, 21, 34, ... Esta série inicia-se com o elemento de ordem 0: 0 e o
elemento de ordem 1: 1, o elemento de ordem n é igual a soma dos elementos de
ordem n-1 e ordem n-2. O programa abaixo mostra uma solução recursiva para
calcular o termo de ordem i:
class programa5{
public static void main(String[] args){
int n=4;
System.out.println("o valor do termo de ordem
"+n+
" da serie de Fibonacci e' "+fib(n));
}
static int fib(int n){
if(n<=0) return 0;
if(n==1) return 1;
return fib(n-1)+fib(n-2);
}
}
A tabela abaixo mostra a
correlação entre o valor de n e o número de registros de ativação de fib():
|
n |
Número de Registros |
valor retornado |
|
0 |
1 |
0 |
|
1 |
1 |
1 |
|
2 |
3 |
1 |
|
3 |
5 |
2 |
|
4 |
9 |
3 |
Veja abaixo o cálculo de dígitos verificadores de CPF revisitado. Foram
utilizados dois métodos: (i) char2intDig e (ii)somaProdPeso.: Não estamos
defendendo que este seja um bom programa para cálculo dos dígitos, a idéia é
exemplificar o uso de métodos. O método char2intDig recebe como parâmetro um
arranjo de caracteres (que devem ser caracteres correspondentes aos digitos de
zero a nove) e devolve (ou retorna) um arranjo de componentes do tipo int com a
representação do tipo int de cada dígito de índice correspondente. O método
somaProdPeso recebe como entrada um arranjo com dígitos e um arranjo com pesos
de cada dígito. Este método devolve um valor do int correpondente à soma do
produto dos dígitos e dos pesos fornecidos. Toda seqüência de comandos para a
qual tenhamos um bom nome é candidata a se transformar em um método! Tente
encontrar neste programa outros trechos que poderiam se transformar em métodos.
No caso de programas pequenos isto é meio artificial, mas no caso de programas
extensos o particionamento do código em métodos é fundamental para facilitar a
escrita e a leitura.
class outrocpf{
static int[] char2intDig(char[] x){
if(x==null) return null;
int[] y=new int[x.length];
for(int i=0; i<x.length; i++)
y[i]=x[i]-'0';
return y;
}
static int somaProdPeso(int[]
digito, int[] peso){
int somaProd=0;
for(int i=0; i<digito.length;
i++) somaProd=somaProd+digito[i]*peso[i];
return somaProd;
}
public static void main(String
args[]){
if(args.length!=1){
System.out.println("Forneca o numero do CPF na linha de
comando");
System.exit(0);
}
char[] cpf=
args[0].toCharArray();
System.out.println("calculo
dos digitos verificadores do cpf:"+args[0]);
int somaprod1, somaprod2;
int dezena, unidade;
int restoAux;
int[] digito=char2intDig(cpf);
somaprod1=somaProdPeso(digito,
new int[]{10,9,8,7,6,5,4,3,2});
restoAux=somaprod1%11;
dezena=restoAux<2 ? 0 :
11-restoAux;
somaprod2=dezena*2+somaProdPeso(digito, new int[]{11,10,9,8,7,6,5,4,3});
restoAux=somaprod2%11;
unidade=restoAux<2 ? 0 :
11-restoAux;
System.out.print(dezena);
System.out.println(unidade);
}
}
Um outro exemplo de
fatoração do programa de cálculo de digitos verificadores em métodos. Nesta
versão não é usado o arranjo de caracteres: é utilizado o String como tipo da
variável que armazena a referência para a instância que aramazena a cadeia de
caracteres do cpf. Observe que a expressão cpf+dezena é do tipo String e a sua avaliação
consiste em converter o operando do tipo int (dezena) em uma instância de
String e então criar uma instância com a dezena concatenada ao cpf original.
class aindacpf{
static int dezena(String cpf){
int somaprod=0;
for(int i=0; i<cpf.length();
i++)
somaprod=somaprod+(cpf.charAt(8-i)-'0')*(i+2);
int restoAux=somaprod%11;
return restoAux<2 ? 0 :
11-restoAux;
}
static int unidade(String cpf10){
int somaprod=0;
for(int i=0; i<cpf10.length();
i++)
somaprod=somaprod+(cpf10.charAt(9-i)-'0')*(i+2);
int restoAux=somaprod%11;
return restoAux<2 ? 0 :
11-restoAux;
}
public static void main(String
args[]){
if(args.length!=1){
System.out.println("Forneca o numero do CPF na linha de
comando");
System.exit(0);
}
String cpf= args[0];
System.out.println("calculo
dos digitos verificadores do cpf:"+cpf);
int dezena=dezena(cpf);
int unidade=unidade(cpf+dezena);
System.out.print(dezena);
System.out.println(unidade);
}
}
Exercícios:
static int indMax(int[] arranjo)
static boolean mesmoTamanho(int[] a1, int[]
a2)
static double somatorioDouble(double[] ad)
static boolean iguais(int[] ai)
static double prodInterno(double[] a1,
double[] a2)
static int fib(int n)
Exercícios extra:
Reesecreva o programa de
multiplicação de matrizes utilizando métodos com nomes ilustrativos da função
do método.
Reescreva os programas
relacionados ao jogo da velha e ao Nurikabe utilizando métodos.