O Programa que Escolhe e Repete — if, switch e Laços

O Programa que Escolhe e Repete — if, switch e Laços

O if e a condição que precisa ser boolean, o switch clássico com fall-through e o moderno com setas, switch expressions e yield, os laços while, do-while e for, break, continue e rótulos, com a sequência de Collatz, um jogo de adivinhação e um menu interativo.
Java

• • 15 min de leitura

As estruturas que dão a um programa a capacidade de tomar decisões e repetir trabalho, do if clássico às switch expressions modernas.

Os programas de "Operadores, Texto e Conversa com o Usuário" executavam sempre as mesmas linhas, na mesma ordem, do começo ao fim. Um programa de verdade precisa escolher o que fazer conforme a situação e repetir uma tarefa quantas vezes for necessário. Esta aula apresenta as estruturas de controle de fluxo do Java: o if, as duas formas do switch, e os laços while, do-while e for. Com elas, a conta do restaurante poderia recusar zero pessoas, e ainda vamos escrever um jogo de adivinhação e um menu interativo.

if: o programa decide

O if executa um bloco de código só quando uma condição é verdadeira. O else oferece o caminho alternativo, e else if encadeia várias possibilidades:

void main() {
    double nota = Double.parseDouble(IO.readln("Nota (0 a 10): "));

    if (nota < 0 || nota > 10) {
        IO.println("Nota inválida.");
    } else if (nota >= 7) {
        IO.println("Aprovado.");
    } else if (nota >= 5) {
        IO.println("Recuperação.");
    } else {
        IO.println("Reprovado.");
    }
}

As condições são testadas de cima para baixo, e só o primeiro bloco verdadeiro executa. Com a nota 8,5, a primeira condição é falsa e a segunda é verdadeira: o programa imprime "Aprovado." e pula todo o resto. Por isso o teste nota >= 5 não precisa verificar também nota < 7, já que só é alcançado quando a nota é menor que 7. A ordem das condições faz parte da lógica.

As chaves delimitam os blocos. Quando um bloco tem uma só instrução, o Java permite omitir as chaves, mas é um convite ao erro: alguém acrescenta uma segunda linha achando que ela está dentro do if, e ela não está. Uma regra que muitos guias de estilo adotam, e que este curso segue: sempre use chaves.

A condição precisa ser boolean

Em C, este código compila e é um dos erros mais famosos da história da programação:

if (x = 10) { ... }   /* em C: atribui 10 a x, e o resultado 10 conta como "verdadeiro" */

Quem escreveu queria comparar (==), mas digitou uma atribuição (=), e o if passa a ser sempre verdadeiro. Em Java, a condição de um if precisa ser um boolean, e o erro é pego na compilação:

E1.java:3: error: incompatible types: int cannot be converted to boolean
    if (x = 10) {
          ^

É a rigidez de tipos de "Cada Valor em sua Caixa" trabalhando a seu favor. O mesmo vale para if (x) com x inteiro, algo comum em C, que em Java não compila. Você escreve if (x != 0), que é mais claro de qualquer forma.

switch: escolhendo entre muitos casos

Quando a decisão depende do valor de uma única variável, com muitos valores possíveis, uma cadeia de if fica repetitiva. Para isso existe o switch. O Java tem duas formas dele, e você precisa conhecer as duas.

A forma clássica, e sua armadilha

A forma herdada do C usa case com dois-pontos e break:

void main() {
    int dia = 6;
    String nome;
    switch (dia) {
        case 1:
            nome = "domingo";
            break;
        case 6:
            nome = "sexta";
        case 7:
            nome = "sábado";
            break;
        default:
            nome = "dia útil";
    }
    IO.println(nome);
}
sábado

O dia é 6 e a resposta foi "sábado". O case 6 não tem break, e na forma clássica a execução cai para o caso seguinte (fall-through): executa nome = "sexta", continua em nome = "sábado" e só para no break. Esquecer um break é um erro fácil de cometer, difícil de ver numa revisão e que o compilador não aponta. Você vai encontrar essa forma em muito código existente, então precisa saber lê-la. Para código novo, há algo melhor.

A forma moderna: setas e switch expressions

Desde o Java 14, o switch aceita casos com seta (->). Com a seta não existe fall-through: cada caso executa só o próprio código. Além disso, o switch pode ser uma expressão, que produz um valor:

void main() {
    String dia = IO.readln("Dia da semana: ").strip().toLowerCase();

    String tipo = switch (dia) {
        case "sábado", "domingo" -> "fim de semana";
        case "segunda", "terça", "quarta", "quinta", "sexta" -> "dia útil";
        default -> "dia desconhecido";
    };

    IO.println(tipo);
}
Dia da semana: Domingo
fim de semana

Várias coisas melhoraram ao mesmo tempo:

  • Um caso pode ter vários valores, separados por vírgula, em vez de uma pilha de case que caem uns nos outros.
  • O resultado é atribuído diretamente a tipo. Não há uma variável preenchida em cada ramo, como nome no exemplo anterior.
  • O switch funciona com String, além de inteiros e char. Com enums e com tipos inteiros de objetos, ele fica ainda mais poderoso, como você vai ver em "Hierarquias Fechadas".
  • O compilador exige que todos os casos sejam cobertos. Uma expressão precisa produzir um valor para qualquer entrada, e sem o default o código não compila:
E2.java:3: error: the switch expression does not cover all possible input values
    String s = switch (n) {
               ^

Repare também no .strip().toLowerCase() na leitura: os métodos de String da aula anterior, encadeados, fazem "Domingo", "domingo" e " DOMINGO " caírem no mesmo caso.

Quando um caso precisa de mais de uma instrução para calcular o valor, ele usa um bloco, e a palavra yield entrega o resultado:

int mes = 2;
boolean bissexto = false;

int diasNoMes = switch (mes) {
    case 2 -> {
        IO.println("Fevereiro depende do ano...");
        yield bissexto ? 29 : 28;
    }
    case 4, 6, 9, 11 -> 30;
    default -> 31;
};

O switch com setas também pode ser usado como instrução, sem produzir valor, para executar ações diferentes em cada caso. O menu no fim desta aula faz exatamente isso. A recomendação para código novo é simples: use sempre a forma com setas.

while: repetir enquanto for verdade

O while repete um bloco enquanto a condição for verdadeira. A condição é testada antes de cada volta, então o bloco pode não executar nenhuma vez:

int contagem = 3;
while (contagem > 0) {
    IO.println("Faltam " + contagem);
    contagem--;
}
IO.println("Lançar!");
Faltam 3
Faltam 2
Faltam 1
Lançar!

Todo laço precisa de algo, dentro dele, que em algum momento torne a condição falsa. Aqui é o contagem--. Sem ele, o laço seria infinito, e o programa ficaria preso, repetindo para sempre. Se isso acontecer, Ctrl+C no terminal interrompe o programa.

O while é a escolha natural quando não se sabe de antemão quantas repetições haverá. Um exemplo clássico é a sequência de Collatz: a partir de um número, divida por 2 se for par ou faça 3n + 1 se for ímpar, e repita até chegar a 1.

void main() {
    int n = Integer.parseInt(IO.readln("Número inicial: "));
    int passos = 0;

    while (n != 1) {
        n = (n % 2 == 0) ? n / 2 : 3 * n + 1;
        passos++;
    }

    IO.println("Chegou a 1 em " + passos + " passos.");
}
Número inicial: 27
Chegou a 1 em 111 passos.

Começando em 6, bastam 8 passos. Começando em 27, são 111. Ninguém sabe provar que essa sequência chega a 1 para todo número inicial. É um problema em aberto da matemática, a conjectura de Collatz. O programa, porém, não precisa saber quantas voltas vai dar: o while repete enquanto for necessário.

do-while: executar pelo menos uma vez

O do-while testa a condição depois de cada volta, então o bloco executa ao menos uma vez. Ele é ideal quando a condição depende de algo que só existe depois da primeira execução, como uma resposta do usuário. Um jogo de adivinhação:

void main() {
    int segredo = (int) (Math.random() * 100) + 1;
    int tentativas = 0;
    int palpite;

    IO.println("Pensei num número de 1 a 100. Tente adivinhar!");
    do {
        palpite = Integer.parseInt(IO.readln("Seu palpite: "));
        tentativas++;

        if (palpite < segredo) {
            IO.println("É maior.");
        } else if (palpite > segredo) {
            IO.println("É menor.");
        }
    } while (palpite != segredo);

    IO.println("Acertou em " + tentativas + " tentativa(s)!");
}

Math.random() devolve um double aleatório entre 0 (incluído) e 1 (excluído). Multiplicado por 100, fica entre 0 e 99,999... O cast para int corta a parte decimal, como você viu em "Cada Valor em sua Caixa", e dá um inteiro de 0 a 99. Somando 1, de 1 a 100. Não faz sentido testar o palpite antes de o jogador dar um, por isso o do-while: primeiro pergunta, depois decide se continua. Repare também que palpite é declarado fora do laço. A condição do while fica fora das chaves e só enxerga variáveis declaradas antes do do.

for: repetir um número conhecido de vezes

Quando o número de repetições é conhecido, o for reúne numa linha as três partes de um laço com contador: a inicialização, a condição e a atualização.

for (int i = 1; i <= 5; i++) {
    IO.print(i * i + " ");
}
IO.println("");
1 4 9 16 25

Lê-se: "comece com i valendo 1; enquanto i <= 5, execute o bloco; depois de cada volta, faça i++". Tudo o que controla o laço fica num lugar só, o que torna o for mais fácil de ler e mais difícil de errar do que um while equivalente. Esquecer o incremento, por exemplo, é bem mais difícil.

A variável declarada no for só existe dentro dele. Tentar usá-la depois do laço dá erro:

Escopo.java:3: error: cannot find symbol
    IO.println(i);
               ^

Isso é uma virtude: uma variável deve existir só enquanto é necessária. O escopo, a região do código onde um nome é válido, volta com mais detalhes em "Dividir para Resolver".

Laços podem ser aninhados, um dentro do outro. O de dentro dá todas as suas voltas a cada volta do de fora:

for (int i = 1; i <= 3; i++) {
    for (int j = 1; j <= 3; j++) {
        IO.print(i + "x" + j + "=" + (i * j) + "  ");
    }
    IO.println("");
}
1x1=1  1x2=2  1x3=3
2x1=2  2x2=4  2x3=6
3x1=3  3x2=6  3x3=9

Existe ainda uma forma do for que percorre coleções de valores, o for-each. Ela precisa de algo para percorrer, e vai aparecer junto com os arrays, na aula "Muitos Valores sob um Nome".

break e continue

Duas instruções alteram o andamento de um laço. O break sai do laço imediatamente. O continue pula o resto da volta atual e passa para a próxima:

int soma = 0;
for (int n = 1; n <= 100; n++) {
    if (n % 3 != 0) {
        continue;       // não é múltiplo de 3: pula para o próximo n
    }
    soma += n;
}
IO.println("Soma dos múltiplos de 3 até 100: " + soma);   // 1683

Com laços aninhados, um break simples sai só do laço mais interno. Para sair de vários de uma vez, o laço externo pode receber um rótulo:

busca:
for (int i = 1; i <= 9; i++) {
    for (int j = 1; j <= 9; j++) {
        if (i * j == 42) {
            IO.println("Achei: " + i + " x " + j);   // Achei: 6 x 7
            break busca;
        }
    }
}

Use rótulos com moderação. Quando um laço precisa deles, costuma ser sinal de que aquele trecho merece virar um método separado, capaz de simplesmente devolver o resultado assim que o encontrar. É uma técnica da próxima aula.

Juntando tudo: um menu que se repete

Um padrão muito comum em programas de terminal: mostrar um menu, executar a opção escolhida e repetir até o usuário pedir para sair. Ele combina do-while, switch com setas e if:

void main() {
    double saldo = 0;
    String opcao;

    do {
        IO.println("""

            1 - Depositar
            2 - Sacar
            3 - Ver saldo
            0 - Sair""");
        opcao = IO.readln("Opção: ").strip();

        switch (opcao) {
            case "1" -> {
                double valor = Double.parseDouble(IO.readln("Valor: "));
                saldo += valor;
            }
            case "2" -> {
                double valor = Double.parseDouble(IO.readln("Valor: "));
                if (valor > saldo) {
                    IO.println("Saldo insuficiente.");
                } else {
                    saldo -= valor;
                }
            }
            case "3" -> IO.println("Saldo: " + saldo);
            case "0" -> IO.println("Até logo!");
            default -> IO.println("Opção inválida.");
        }
    } while (!opcao.equals("0"));
}

Um trecho de uma execução, depositando 100, sacando 30 e tentando sacar 500:

Opção: 3
Saldo: 70.0

1 - Depositar
2 - Sacar
3 - Ver saldo
0 - Sair
Opção: 0
Até logo!

Repare em três detalhes. A opção é tratada como texto: comparar String com "1" evita a conversão e o erro que ela causaria se o usuário digitasse uma letra. A condição do laço usa equals, e não ==, pela lição da aula anterior. E a variável valor é declarada duas vezes, em dois blocos diferentes, sem conflito, porque cada uma só existe dentro das próprias chaves.

O programa funciona, mas o main já está ficando comprido, com a leitura de valores repetida e responsabilidades misturadas. Em programas maiores isso vira um problema sério. O remédio é dividir o programa em partes com nome e responsabilidade próprios: os métodos.

Fontes e leituras recomendadas

Exercícios

Exercício 1

No switch clássico do início da aula, o dia 6 imprimiu "sábado". Explique por quê e reescreva o trecho com switch expression, de modo que o problema não possa acontecer.

Ver resposta

✓ Resposta: O case 6 não tem break. Na forma clássica, a execução continua no caso seguinte (fall-through): nome recebe "sexta" e logo depois "sábado", até encontrar o break do case 7. Reescrito:

String nome = switch (dia) {
    case 1 -> "domingo";
    case 6 -> "sexta";
    case 7 -> "sábado";
    default -> "dia útil";
};

Com setas não existe fall-through: cada caso produz o próprio valor e termina. Como o switch agora é uma expressão, o compilador também garante que todo valor possível de dia é coberto, graças ao default, e que nome sempre recebe um valor.

Exercício 2

Escreva uma switch expression que receba o número de um mês (1 a 12) e devolva o trimestre ("1º trimestre" a "4º trimestre"), ou "mês inválido".

Ver resposta

✓ Resposta:

void main() {
    int mes = Integer.parseInt(IO.readln("Mês (1 a 12): "));

    String trimestre = switch (mes) {
        case 1, 2, 3 -> "1º trimestre";
        case 4, 5, 6 -> "2º trimestre";
        case 7, 8, 9 -> "3º trimestre";
        case 10, 11, 12 -> "4º trimestre";
        default -> "mês inválido";
    };

    IO.println(trimestre);
}

Os vários valores por caso deixam a tabela legível de relance. Outra solução, com aritmética, seria (mes - 1) / 3 + 1, usando a divisão inteira. Ela é mais curta, mas precisaria de um if à parte para validar o mês, e esconde a intenção. Em regras de negócio, a clareza do switch costuma valer mais que a esperteza da fórmula.

Exercício 3

Escreva um programa que leia um número inteiro positivo e mostre a soma dos seus dígitos. Por exemplo, 2026 deve dar 10.

Ver resposta

✓ Resposta:

void main() {
    int numero = Integer.parseInt(IO.readln("Número: "));
    int soma = 0;

    while (numero > 0) {
        soma += numero % 10;   // pega o último dígito
        numero /= 10;          // remove o último dígito
    }

    IO.println("Soma dos dígitos: " + soma);
}

Com 2026: o resto por 10 dá 6 e o número vira 202; depois 2 e 20; depois 0 e 2; depois 2 e 0. A soma é 6 + 2 + 0 + 2 = 10. O while é a escolha certa, porque a quantidade de voltas depende da quantidade de dígitos, que não se conhece de antemão. O % e o / inteiros da aula anterior fazem o trabalho de separar os dígitos.

Exercício 4

No programa da sequência de Collatz, por que o laço usado é while e não for? E por que o jogo de adivinhação usa do-while e não while?

Ver resposta

✓ Resposta: O for é indicado quando se conhece o número de repetições, ou ao menos a faixa que o contador vai percorrer. Na sequência de Collatz, não há como saber quantos passos serão necessários: 6 precisa de 8 e 27 precisa de 111. O laço só pode ser controlado pela condição n != 1, que é o papel do while. No jogo, a condição de parada (palpite != segredo) depende de um palpite que ainda não existe antes da primeira volta. O do-while executa o bloco pelo menos uma vez e só então testa a condição, o que casa exatamente com "pergunte primeiro, depois decida se continua". Com while, seria preciso inventar um valor inicial artificial para palpite, só para entrar no laço.

Exercício 5

O programa abaixo verifica se um número é primo testando divisores até a raiz quadrada. Ele funciona para 97, mas com 2147483647, que é primo, demora vários segundos e responde "não é primo". Explique o defeito e corrija.

void main() {
    int n = Integer.parseInt(IO.readln("Número: "));
    boolean primo = n >= 2;
    for (int d = 2; d * d <= n; d++) {
        if (n % d == 0) {
            primo = false;
            break;
        }
    }
    IO.println(n + (primo ? " é primo." : " não é primo."));
}
Ver resposta

✓ Resposta: O defeito é o overflow de "Cada Valor em sua Caixa". Com n = 2.147.483.647 (o maior int), d * d passa do limite do int quando d chega a 46.341 e dá a volta para um número negativo, que continua sendo <= n. O laço nunca para pela condição, e d segue até alcançar o próprio n, onde n % d == 0 é verdadeiro, e o programa conclui erradamente que o número não é primo, depois de mais de dois bilhões de voltas. A correção é fazer a multiplicação em long:

for (int d = 2; (long) d * d <= n; d++) {

Com isso, d * d nunca estoura, o laço para em d = 46.341, e a resposta sai correta em milissegundos. Outra forma é escrever a condição como d <= n / d, que evita a multiplicação. O exercício junta as três aulas até aqui: um laço, uma condição e um limite de tipo que ninguém vê até ele causar um erro.

Comentários

Mais em Java

Muitos Valores sob um Nome — Arrays
Muitos Valores sob um Nome — Arrays

Arrays em Java: criação, índices, valores padrão, limites vigiados pela JVM…

Recursos que se Fecham Sozinhos — try-with-resources, Arquivos e NIO.2
Recursos que se Fecham Sozinhos — try-with-resources, Arquivos e NIO.2

Como ler e gravar arquivos com a API NIO.2 do Java (Path e Files), tratar a…

Do Geral ao Específico — Herança e a Classe Object
Do Geral ao Específico — Herança e a Classe Object

Herança em Java: extends, a chamada a super e a ordem de construção…