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
caseque caem uns nos outros. - O resultado é atribuído diretamente a
tipo. Não há uma variável preenchida em cada ramo, comonomeno exemplo anterior. - O
switchfunciona comString, além de inteiros echar. 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
defaulto 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
- The Java Language Specification, Java SE 25, capítulo 14, "Blocks, Statements, and Patterns": https://docs.oracle.com/javase/specs/jls/se25/html/jls-14.html. A definição formal do
if, dos laços, dobreak, docontinuee dos rótulos. - JEP 361, "Switch Expressions": https://openjdk.org/jeps/361. A proposta que tornou definitivos o
switchcom setas e as switch expressions no Java 14, com os motivos para cada decisão, inclusive oyield. - Oracle, The Java Tutorials, "Control Flow Statements": https://docs.oracle.com/javase/tutorial/java/nutsandbolts/flow.html. Um tutorial antigo, que mostra apenas o
switchclássico, mas é útil para verwhile,do-whileeforcom outros exemplos. - Dev.java, "Branching with Switch Statements" e "Branching with Switch Expressions": https://dev.java/learn/language-basics/switch-expressions/. O tutorial oficial moderno sobre as duas formas do
switch. - Jeffrey Lagarias, "The 3x+1 Problem and its Generalizations" (1985): https://www.cecm.sfu.ca/organics/papers/lagarias/paper/html/paper.html. Para quem ficou curioso com a sequência de Collatz, o artigo clássico sobre o problema.
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.