6.2. A instrução for

Em Java há várias instruções que podem ser usadas para repetir ações. São aquilo a que se chama ciclos (ou loop em inglês).

Começamos por estudar o ciclo for, que deve ser usado quando queremos exprimir repetição limitada, ou seja, quando o número máximo de vezes que o ciclo é executado está previamente determinado.

6.2.1. Sintaxe e semântica

A sintaxe geral do ciclo for pode ser descrita por:

A guarda é uma expressão do tipo boolean (ou condição).

O corpo-do-ciclo é um bloco de instruções entre chavetas.

O progresso é uma instrução.

O fluxo de execução do ciclo for é representado pela seguinte figura.

Um exemplo:

Aqui a inicialização é int i = 1, a guarda é i <= n, o progresso é i++ e o corpo do ciclo é System.out.print("Blah");. Quando executadas, estas instruções escrevem BlahBlahBlahBlah. no ecrã.

Vamos “acompanhar” a execução deste ciclo, guiando-nos pelo fluxograma da figura acima.

A variável i é inicializada a 1. Porque 1 é menor ou igual ao valor de n (que é 4), então o corpo do ciclo é executado escrevendo Blah no ecrã (não há mudança de linha pois é print e não println). De seguida a instrução i++ é executada, o que faz com que o valor da variável i seja incrementado de uma unidade. E agora volta-se a repetir o esquema anterior – guarda, corpo-do-ciclo, progresso – até uma altura em que i é incrementado para o valor 5 que, já não sendo <= 4, torna falsa a guarda, fazendo com que o ciclo termine. Neste processo foi escrita a palavra Blah mais 3 vezes, na mesma linha. Já fora do ciclo, é ainda escrito um ponto.

Chamamos variável de progresso à variável i pois as mudanças que ela sofre fazem a execução do ciclo progredir em direção ao fim do ciclo, o qual se dá quando a guarda i <= n deixa de se verificar.

6.2.2. Ciclos infinitos

Quando o progresso não garante que a guarda se torne falsa, então temos um ciclo infinito, isto é, um ciclo que nunca pára.

Na verdade, a guarda representa uma condição de continuação, pois enquanto for verdadeira, o ciclo continua a sua execução.

O seguinte pedaço de código ilustra um ciclo infinito.

Repare que a variável de progresso – i – é inicializada a 1 e que a guarda é verdadeira enquanto o valor de i for diferente de zero. A guarda é sempre verdadeira pois os valores que i vai tomando são os inteiros maiores que zero, os quais são sempre diferentes de zero.

NOTA: Na verdade, quando o valor de i se tornar tão grande quanto o valor máximo do conjunto de valores para o tipo int, a soma de uma unidade fará com que o valor de i se torne negativo (-231) devido aos mecanismos de representação e de adição em binário; assim, mais cedo ou mais tarde o ciclo terminará pois i tomará o valor zero inevitavelmente. Embora não se verifique nenhum erro de execução, no entanto essa é uma situação a evitar a todo o custo, pois conceptualmente este é um ciclo infinito.

6.2.3. Variações nas componentes do ciclo

Nos exemplos anteriores, o corpo do ciclo tem sempre o mesmo efeito, que é o de escrever a palavra Blah no ecrã. O próximo exemplo mostra um ciclo onde o corpo do ciclo tem um efeito diferente de cada vez que é executado porque contém uma parte que varia à medida que o ciclo progride.

Quando executadas, estas instruções escrevem

no ecrã. Tente certificar-se de que é isso que acontece, guiando-se pelo fluxograma que descreve o ciclo for, à semelhança do que foi feito no exemplo anterior.

Mudando ligeiramente as várias partes do ciclo, podemos alterar o seu efeito. Por exemplo, quando executadas, as seguintes instruções

escrevem o seguinte no ecrã

Repare que o corpo do ciclo é executado também quatro vezes, mesmo sendo diferentes as inicialização, guarda e progresso.

O ciclo seguinte tem um efeito exatamente igual no ecrã:

6.2.4. Imprimir tabuadas

E se quisermos agora imprimir a tabuada do 7? Esta tabuada obtém-se multiplicando por 7 os números naturais de 1 a 10.

Para decidir como fazer isto em programação, devemos reparar que basta repetir uma mesma ação dez vezes. Essa ação pode-se descrever como “imprimir o resultado de multiplicar 7 por um número natural”, sabendo que a primeira vez esse número natural será 1, da segunda vez será 2, etc.

Então, se chamarmos i a esse número natural que varia, basta darmos valores sequenciais a i, partindo do valor 1, até chegar a 10, repetindo, de cada vez, a ação “imprimir o resultado de multiplicar 7 por i.

Então, o programa seguinte:

quando executado, escreve no ecrã (abreviamos com … para poupar espaço)

Podemos tornar o output mais informativo, explicitando a que expressões correspondem os vários valores:

Este programa, quando executado, escreve no ecrã

Note que, como a multiplicação (*) tem prioridade sobre a concatenação (+), na avaliação da expressão "Linha " + i * 10, o valor da sub-expressão i * 10 é calculado primeiro. De seguida, esse valor é concatenado com a string "Linha ", exatamente como pretendido.

 

Estes programas têm um interesse limitado pois, sempre que são invocados, escrevem no ecrã a mesma tabuada do 7.

Na secção 6.3. veremos como generalizar este cálculo e impressão de tabuada, construindo métodos que permitam fazê-lo para quaisquer valores, tanto do número da tabuada, como do número de valores apresentados.

6.2.5. Calcular somatórios

E se agora quisermos que o computador calcule e imprima a soma dos inteiros positivos menores que 10, por exemplo? Numa atitude de mínimo esforço mental, podemos fazê-lo avaliar a expressão 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 :

Mas o que acontece quando queremos a soma dos inteiros positivos menores que 100? Ou menores que 1000? Pois... esta solução não é nada prática; é mesmo muito idiota do ponto de vista de programação.

Mas mesmo que quisessemos adotar esta abordagem, há casos em que nem sequer conseguiríamos! Quais? Aqueles em que o limite superior só é decidido quando o código é executado, ou seja, o programador não o conhece quando escreve o programa.

Um exemplo típico (e que veremos mais adiante) é aquele em que esse valor é decidido pelo utilizador que executa o programa. Mas, para já, podemos focar-nos no caso em que esse valor é dado pelo parâmetro de um método.

Se quisermos construir um método que calcule e devolva a soma dos inteiros positivos menores que um dado número, que é dado por um parâmetro, teremos que adotar outra abordagem, mais esperta, pois não conhecemos esse número quando escrevemos a definição do método.

Supondo que a assinatura do método é:

Como não conseguimos escrever e avaliar uma expressão que represente a soma completa dos inteiros no intervalo [1, n – 1], então o corpo do método vai ter que conter instruções que digam ao computador para fazer essa soma inteiro a inteiro, até ter conseguido somar todos os valores no intervalo.

Para somar os inteiros um a um, vamos ter que ser capazes de adicionar cada novo inteiro à soma dos inteiros já processados. Por exemplo, se os inteiros entre 1 e 3 já tiverem sido processados, teremos que adicionar o 4 a 6 (que é a soma de 1, 2 e 3), de seguida o 5 a 10 e assim sucessivamente até termos somado todos os inteiros necessários.

Podemos descrever este comportamento repetitivo com a frase: “adicionar cada novo número à soma já acumulada”.

A "soma já acumulada” é o valor que temos que manter em memória até lhe adicionarmos um novo número. Vamos então usar uma variável para guardarmos a soma acumulada.

Embora o valor da soma acumulada vá mudando à medida que somamos cada novo número, só o último valor que ela toma nos interessa a cada instante. Então, basta-nos uma única variável, pois podemos atribuir-lhe novos valores tantas vezes quantas quisermos. Chamemos soma a essa variável.

A ação que queremos repetir pode ser codificada em Java pela instrução: soma = soma + numero; em que a variável numero representa o novo número a somar de cada vez. Esta instrução constituirá o corpo do nosso ciclo.

A instrução que queremos é, de forma ainda incompleta:

Queremos que, a cada repetição da ação, a variável numero tome um valor diferente que, neste caso, é o de sucessor do valor que foi processado na repetição anterior.

Para obter o sucessor de um número inteiro basta acrescentar-lhe uma unidade; então o progresso da nossa repetição pode ser numero++; numero é a variável de progresso do ciclo.

Já podemos completar alguns dos "buracos" que nos faltam:

Qual o valor inicial para a variável numero? É 1, pois queremos a soma desde 1 até n - 1.

E qual é a condição de continuação do ciclo? É numero < n porque queremos acumular os inteiros inferiores a n. Então será que o método seguinte está completo?

Ainda não... O que falta? Falta a instrução return, que permite terminar o método devolvendo o resultado.

O resultado é o conteúdo da variável soma, que foi usada para ir acumulando os vários inteiros positivos menores que n. No fim do ciclo o seu valor é precisamente o valor do somatório que pretendemos.

Então, a última instrução do método deve ser return soma;.

Não falta mais nada? Falta! Falta a inicialização da variável soma. Com que valor?

Para que o primeiro inteiro a somar (neste caso o 1) possa ser processado da mesma maneira que os valores seguintes – “adicionar cada novo número à soma já acumulada” –, a variável soma tem que ter um valor inicial que represente a situação em que nada foi acumulado ainda, ou seja zero.

NOTA: Repare que zero é precisamente o elemento neutro da adição. Se estivessemos a calcular o produto dos inteiros positivos menores que n, como seria o corpo do ciclo? E a inicialização da variável que guarda o produto acumulado?

Resumindo e concluindo, a versão final do método é:

 

O ciclo seguinte tem exatamente o mesmo efeito final que o ciclo do método somaPositivosMenoresQue.

Qual a diferença? É somente a forma como são gerados os inteiros positivos menores que n. Nesta versão é do maior para o menor – a variável de progresso numero é inicializada com o maior dos números a somar (n - 1) e é decrementada de uma unidade a cada execução do corpo do ciclo. A guarda, ou condição de continuação, é, neste caso, numero >= 1 pois enquanto a variável numero tiver um valor maior ou igual a 1, o seu valor deve ser acumulado na variável soma.

6.2.6. Casos críticos

Sempre que construímos um ciclo, devemos pensar em resolver o caso geral. Quando estamos convencidos que funciona bem para o caso geral, devemos investigar se também funciona bem para os casos críticos.

No exemplo da soma dos inteiros, devemos investigar:

Vamos estudar os casos em que n é menor ou igual a 1. Vamos ver se o corpo do método calcula bem o resultado, que deve ser zero:

A variável soma começa por tomar o valor zero. O fluxo de execução do ciclo for determina que o primeiro bloco a ser executado é a inicialização, ou seja, neste caso a variável numero fica com o valor 1.

De seguida, a guarda é avaliada. Neste caso a guarda vale false pois como n é menor ou igual a 1, a variável numero não é menor que n.

Então o fluxo de execução segue imediatamente para a instrução a seguir ao ciclo, sem nunca executar o corpo do ciclo. A instrução return soma faz com que o método termine a sua execução devolvendo o resultado zero, como pretendido.

No caso de n ter o valor 2, então o resultado deverá ser 1. A variável soma começa por tomar o valor zero. A execução da inicialização atribui o valor 1 à variável numero.

De seguida a avaliação da guarda conclui que esta vale true. Então o fluxo de execução segue para o corpo do ciclo, acumulando o valor da variável numero na variável soma, ficando esta com o valor 1.

De seguida o fluxo de execução segue para a execução do progresso, incrementando a variável numero de uma unidade.

A guarda é de novo avaliada dando false desta vez. Segue-se a instrução return soma que termina a execução devolvendo o resultado 1, como pretendido.

Tente agora construir uma função para calcular o produtório de um dado número. Comece por considerar o caso geral. De seguida identifique os casos críticos e investigue se a sua solução funciona bem. Recorde que o elemento neutro do produto é 1.

6.2.7. Reforço da guarda

Vamos programar uma função que calcule e devolva o menor divisor maior que 1 de um dado número n maior que 1, com a seguinte assinatura e documentação:

Sabendo que um número i é divisor de um número n se o resto da divisão de n por i é zero, então podemos começar por testar se n % i == 0, para i igual a 2. Se for esse o caso, então 2 é o menor divisor de n maior que 1.

Se não for, fazemos o mesmo para i igual a 3 e assim sucessivamente; temos então um ciclo for com i como variável de progresso, inicializada a 2, progredindo de 1 em 1.

Logo que encontremos o divisor que procuramos, vamos guardá-lo na memória para depois podermos devolver o seu valor como resultado da função. Para isso usamos uma variável do mesmo tipo do resultado – int neste caso.

Qual poderá ser a guarda do ciclo? Não vale a pena testar o resto da divisão de n por valores maiores que ele próprio, pois um divisor de um número é sempre menor ou igual ao próprio número. Então podemos ter i <= n como guarda.

Esta análise já nos permitiu encontrar uma primeira abordagem para esboçar uma solução:

Esta solução tem vários problemas, uns mais graves que outros.

Qualquer que seja o número n, sabemos que não há divisores entre n/2 e n. Então vamos otimizar o nosso ciclo evitando que sejam feitos cálculos e testes desnecessários. Vamos mudar a guarda para i <= n/2.

Chega? Não, pois sabemos que se n for número primo, o menor divisor de n maior que 1 é o próprio n. Então temos que nos certificar que, se a instrução resultado = i; nunca for executada, a variável resultado terá o valor de n no fim do ciclo. Como fazemos isso? Inicializando-a com o valor de n antes do ciclo. Então temos:

A inicialização da variável resultado teria que ser feita mesmo que não quiséssemos. Se não a inicializássemos, o compilador detetaria que o seu valor vai ser usado como retorno da função e não teria a certeza que um valor lhe seria atribuído – a única instrução que lhe atribui um valor está dentro do bloco de uma instrução condicional (cuja condição pode ser falsa) que, por sua vez, está dentro de um ciclo (que pode nunca ser efetivamente executado).

Para se aperceber de outro problema desta solução, experimente agora simular a execução do método para um valor de n de 15. A instrução resultado = i; é executada quando i tem o valor 3 e é esse o resultado que pretendemos.

Só que a execução do ciclo continua, pois o progresso incrementa o valor de i para 4 e a guarda continua verdadeira.

Então, quando i toma o valor 5, a instrução resultado = i; volta a ser executada, deixando a variável resultado com o valor 5. Oops! E o 3? Já o perdemos!

Devíamos ter parado a execução do ciclo logo que encontrássemos o primeiro divisor maior que 1.

Para isso vamos fazer o que se chama de reforço da guarda, ou seja, vamos tornar a guarda mais restrita, de modo a ser satisfeita num número menor de situações. Neste caso só queremos executar o corpo do ciclo enquanto não encontrarmos o tal divisor que procuramos.

Quando é que sabemos que encontrámos o divisor que procurávamos? Repare que a variável resultado, cujo valor inicial é n, muda de valor a primeira vez logo que é encontrado o primeiro divisor.

Então, logo que a variável resultado muda de valor, ou seja, logo que deixa de ser igual a n, podemos parar.

Basta-nos então reforçar a guarda com uma condição que seja verdadeira enquanto a variável resultado tiver o valor de n.

Com isto podemos exprimir que não basta o i ser menor ou igual a n/2 para continuarmos à procura do divisor. É preciso também que ainda não o tenhamos encontrado.

Repare no operador lógico que usámos para reforçar a guarda. Se queremos caracterizar as situações que satisfaçam ambas as condições i <= n / 2 e resultado == n, então é a conjunção (&&) que devemos usar!

Poderá agora perguntar-se: será que não bastaria a guarda resultado == n? Pense no caso em que n é um número primo, por exemplo 7, e verifique que o ciclo não teria fim pois a variável resultado teria sempre o valor n – a partir do momento em que fosse inicializada e mais tarde quando se verificasse que n é divisor dele próprio.

Só para exercitar a mente, se tivéssemos usado a disjunção (||), ficando a guarda i <= n / 2 || resultado == n, que situações estaríamos a caracterizar com essa guarda? Era isso que queríamos? Uma pista: teríamos obtido um ciclo infinito sempre que n fosse um número primo... Outra pista: obteríamos o maior divisor menor que n, se n não fosse primo...

 

Uma alternativa ao reforço da guarda

Em funções cujo principal objetivo é o de encontrar um dado valor e logo devolver esse valor, torna-se mais simples terminar a execução da função logo que o valor é encontrado, retornando-o como resultado.

Nestes casos, a guarda não precisa de ser reforçada pois a terminação da execução do ciclo (e da própria função) é provocada por uma instrução return.

O ciclo que usamos no método menorDivisorMaiorQueUm tem por único objetivo encontrar um dado valor e logo devolver esse valor; a seguinte versão, sem reforço explícito da guarda, é uma boa alternativa à solução estudada acima:

Nesta versão, embora o trio inicialização-guarda-progresso indicie que o corpo do ciclo vai ser executado n / 2 – 1 vezes, isso pode não acontecer; basta que n tenha mais divisores além de 1 e dele próprio para que o ciclo termine antes que a guarda se torne falsa, terminando com isso a própria função e tendo como resultado o valor encontrado.

Em termos de legibilidade dos programas isto pode ser negativo, mas é compensado pela forma extremamente natural como representa a ideia subjacente de terminar a procura logo que o valor é encontrado, devolvendo-o como resultado da função.

Encontramos este padrão de comportamento em muitas outras situações, para as quais uma abordagem deste tipo é também adequada.

6.2.8. Nunca fazer!

Uma solução que alguns (maus) programadores sugeririam, em alternativa ao reforço da guarda que fizemos na secção anterior, é a seguinte:

O que se fez aqui? Em vez de reforçar a guarda testando se o valor de resultado mudou, mudou-se o valor de i para um valor que falsifica a guarda (n neste caso), de modo a que o ciclo termine logo a seguir.

Nunca se deve fazer este tipo de coisas, pois piora muito a legibilidade dos programas! A variável de progresso i representa, neste ciclo, o número “candidato” a divisor de n e o progresso diz-nos claramente que i progride de uma em uma unidade. Quando mudamos o valor da variável de progresso abruptamente no corpo do ciclo, estamos a alterar esta perceção e a dificultar a compreensão do programa.

Como regra de ouro do ciclo for temos:

“Não se deve alterar o valor da variável de progresso dentro do corpo do ciclo”.

 

6.2.9. O significado das variáveis

O significado que damos às variáveis tem que ser claro desde o início, a bem da legibilidade e facilidade de manutenção dos programas.

Por exemplo, posso “ler” o ciclo do método menorDivisorMaiorQueUm, de forma literal, da seguinte forma:

enquanto o valor de i for menor ou igual a n/2 e o valor de resultado for igual a n, fazer:

Mas na verdade, dado o significado que atribuímos às variáveis, essa leitura seria mais clara se feita da seguinte forma:

enquanto não tivermos testado todos os potenciais valores para divisores de n nem encontrado o divisor que procurávamos, fazer:

Como programadores, devemos ler as instruções do nosso programa de acordo com o significado que fomos atribuindo às variáveis e aos métodos, abstraindo assim dos detalhes irrelevantes das várias instruções que usamos; isto ajuda-nos muito a pensar sobre os problemas a resolver e a entender o que vamos fazendo.

 


 

Anterior: 6.1. Atalhos para operações comuns

Seguinte: 6.3. Ciclos for encaixados