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 deste tipo. Alguns
nomes para esta abstração: procedimento (procedure), subrotina
(subroutine) e função (function).
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(). É o sistema de execução Java quem invoca ou chama o método main().
Antes de podermos chamar ou invocar um método preciso 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 a 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 a 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 outro mecanismos de amarramento (binding) de parâmetros em outras linguagens.
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 do método deve terminar através
do seguinte comando:
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 junto aos comandos de retorno são
de tipo int.
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.
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
de ativação |
valor
retornado |
| 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
de ativação |
valor retornado |
| 0 | 1 | 0 |
| 1 | 1 | 1 |
| 2 | 3 | 1 |
| 3 | 5 | 2 |
| 4 | 9 | 3 |
Exercícios: