Roadmap Java - Parte 7: Estruturas Condicionais, Switch-Case, Escopo e Valores Padrão
- #Java
Este é o sétimo artigo da série Roadmap Java. Recapitulando rapidamente: nas Partes 1 e 2, demos os primeiros passos e montamos o ambiente de desenvolvimento; nas Partes 3, 4 e 5, conhecemos variáveis, tipos, operadores e String; e na Parte 6, vimos entrada e saída de dados com Scanner.
Com isso, já temos variáveis, tipos, operadores e a capacidade de conversar com o usuário, mas todo programa que escrevemos até aqui roda sempre a mesma sequência de instruções, do início ao fim, sem nunca decidir nada. Neste artigo, nós vamos:
- aprender a usar estruturas condicionais (
if,else,else if, operador ternário) para tomar decisões no código, incluindo o Pattern Matching forinstanceof; - conhecer a estrutura
switch-case, uma alternativa para quando há muitos casos possíveis (do formato clássico até a versão moderna, com switch expressions e Pattern Matching); - entender o que é escopo de variável e onde cada variável pode (ou não) ser acessada;
- conhecer os valores padrão que variáveis recebem automaticamente em determinadas situações.
Como usar estruturas condicionais em Java
Estruturas condicionais tomam decisões com base em uma condição, usando a palavra-chave if:
public class Main {
public static void main(String[] args) {
int idade = 20;
if (idade >= 18 && idade <= 40) {
System.out.println("você pode usar o programa");
}
}
}
Se a condição dentro dos parênteses for avaliada como true, o bloco de código entre chaves é executado. Esse trecho entre { e } é chamado de bloco de código, e vamos usar bastante esse termo daqui para frente, principalmente quando falarmos de escopo, mais adiante neste mesmo artigo.
Repare que a condição usada aqui (idade >= 18 && idade <= 40) é a mesma combinação de operador relacional e operador lógico que já vimos na Parte 4. Estruturas condicionais são, no fundo, o principal lugar onde esses operadores relacionais e lógicos entram em ação.
Tratando o caso contrário com else
Para tratar o que acontece quando a condição é falsa, use else:
public class Main {
public static void main(String[] args) {
int idade = 20;
if (idade >= 18 && idade <= 40) {
System.out.println("você pode usar o programa");
} else {
System.out.println("você não pode usar o programa");
}
}
}
O bloco do else só é executado se a condição do if correspondente for falsa. Nunca os dois blocos rodam ao mesmo tempo.
Encadeando condições com else if
Quando existe mais de duas possibilidades, você pode encadear condições com else if:
public class Main {
public static void main(String[] args) {
int idade = 50;
boolean estudante = true;
boolean associado = false;
if (idade >= 18 && idade <= 40) {
System.out.println("você pode usar o programa");
} else if (estudante || associado) {
System.out.println("você pode usar o programa por tempo limitado");
} else {
System.out.println("você não pode usar o programa");
}
}
}
Java avalia as condições em ordem, de cima para baixo. Assim que uma delas é verdadeira, o bloco correspondente é executado e as demais são simplesmente ignoradas — mesmo que também fossem verdadeiras. Só se nenhuma condição bater é que o bloco else final é executado.
Condicionais aninhadas
Também é possível colocar um if dentro de outro — o que chamamos de aninhamento:
public class Main {
public static void main(String[] args) {
int idade = 20;
if (idade >= 18 && idade <= 40) {
boolean estudante = true;
boolean associado = false;
if (estudante || associado) {
System.out.println("você pode usar o programa");
}
} else {
System.out.println("você não pode usar o programa");
}
}
}
Nesse caso, o if interno (estudante || associado) só chega a ser avaliado se o if externo (idade >= 18 && idade <= 40) já tiver sido verdadeiro primeiro. Se o if externo for falso, o if interno nem chega a ser considerado (o programa já vai direto para o else).
Esse exemplo, aliás, não foi escolhido por acaso: ele vai ser a base para explicarmos escopo de variável mais adiante.
Operador ternário
Quando um if-else serve apenas para decidir qual valor atribuir a uma variável (sem nenhuma outra lógica envolvida ), o Java oferece uma forma mais compacta de escrever isso: o operador ternário, com a sintaxe condição ? valorSeVerdadeiro : valorSeFalso.
public class Main {
public static void main(String[] args) {
int idade = 20;
String status = (idade >= 18) ? "maior de idade" : "menor de idade";
System.out.println(status); // maior de idade
}
}
Isso é equivalente a escrever:
public class Main {
public static void main(String[] args) {
int idade = 20;
String status;
if (idade >= 18) {
status = "maior de idade";
} else {
status = "menor de idade";
}
System.out.println(status); // maior de idade
}
}
O nome "ternário" vem do fato de ele ser o único operador em Java que trabalha com três operandos: a condição, o valor caso ela seja verdadeira e o valor caso seja falsa. Ele é chamado de operador, e não de estrutura condicional, justamente porque toda a expressão inteira é avaliada e devolve um único valor (diferente do if-else, que executa blocos de código, mas não devolve nada por si só).
É possível encadear operadores ternários para simular várias condições, mas isso rapidamente prejudica a legibilidade do código:
public class Main {
public static void main(String[] args) {
int idade = 15;
String faixa = (idade < 13) ? "criança" : (idade < 18) ? "adolescente" : "adulto";
System.out.println(faixa); // adolescente
}
}
Como regra prática: use o operador ternário para decisões simples e de uma única condição — ele deixa o código mais curto sem perder clareza. Para qualquer coisa mais elaborada (múltiplas condições encadeadas, ou blocos que fazem mais do que apenas atribuir um valor), prefira um if-else tradicional, mesmo que ele ocupe mais linhas.
Pattern Matching for instanceof (Java 16)
Antes de fecharmos a seção de condicionais, vale conhecer mais uma modernização da linguagem — essa, aliás, é a antecessora direta do Pattern Matching for Switch que vamos ver daqui a pouco.
O operador instanceof sempre existiu em Java para verificar se um objeto é de determinado tipo, mas, na forma clássica, ele só devolve um boolean (para efetivamente usar o objeto como aquele tipo, era preciso fazer um cast manual logo em seguida):
public class Main {
public static void main(String[] args) {
Object valor = "Olá, mundo!";
if (valor instanceof String) {
String texto = (String) valor; // cast manual, repetindo o tipo
System.out.println("Tamanho do texto: " + texto.length());
}
}
}
A partir do Java 16, o instanceof ganhou Pattern Matching: ao testar o tipo, você já pode declarar, na mesma linha, a variável que vai receber o valor convertido (sem cast manual):
public class Main {
public static void main(String[] args) {
Object valor = "Olá, mundo!";
if (valor instanceof String texto) {
System.out.println("Tamanho do texto: " + texto.length());
}
}
}
Aqui, texto só existe (e só é acessível) dentro do bloco do if onde o instanceof deu certo (um caso a mais para o conceito de escopo que vamos formalizar já na próxima seção). Se valor não for uma String, o if simplesmente não entra, e texto nunca chega a existir.
Esse recurso também pode ser combinado com && para adicionar uma condição extra sobre o valor já convertido, numa única linha:
public class Main {
public static void main(String[] args) {
Object valor = "Olá, mundo!";
if (valor instanceof String texto && !texto.isEmpty()) {
System.out.println("Texto recebido: " + texto);
}
}
}
Guarde esse exemplo: a ideia de testar um tipo e já extrair o valor convertido para uma variável, na mesma expressão, é exatamente o mesmo princípio por trás do Pattern Matching for Switch que vamos detalhar a seguir (só que aplicado ao if, em vez de ao switch).
O que é uma estrutura switch-case?
Além dos blocos if-else, o Java também oferece o switch, que permite comparar uma única variável com vários valores possíveis, sem precisar empilhar vários else if seguidos:
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.print("Qual é o primeiro número? ");
int a = scanner.nextInt();
scanner.nextLine();
System.out.print("Qual é o segundo número? ");
int b = scanner.nextInt();
scanner.nextLine();
System.out.print("Qual operação você quer realizar (soma/sub/mul/div)? ");
String operacao = scanner.nextLine();
switch (operacao) {
case "soma":
System.out.printf("%d + %d = %d", a, b, a + b);
break;
case "sub":
System.out.printf("%d - %d = %d", a, b, a - b);
break;
case "mul":
System.out.printf("%d * %d = %d", a, b, a * b);
break;
case "div":
if (b == 0) {
System.out.print("Não é possível dividir por zero!");
} else {
System.out.printf("%d / %d = %d", a, b, a / b);
}
break;
default:
System.out.printf("Operação inválida!");
}
scanner.close();
}
}
Esse é um pequeno programa de calculadora, e reúne vários conceitos que já vimos nos artigos anteriores: Scanner para capturar a entrada do usuário (Parte 6), String.format/printf para formatar a saída (Parte 5), e agora o switch para decidir qual operação executar.
O funcionamento é direto: cada case verifica se o valor da variável testada no switch (no exemplo, operacao) corresponde àquele valor específico. Se corresponder, o bloco daquele case é executado. Se nenhum case corresponder a nada, o bloco default é executado — funcionando como uma espécie de "senão" do switch, equivalente ao else de uma cadeia de if.
Sobre o break
Repare que cada case termina com break. Esse comando interrompe a execução do switch, evitando que o programa "caia" para o próximo case depois de já ter executado o bloco correspondente.
Se você remover os break, todos os casos a partir do que deu match são executados em sequência, um atrás do outro, até encontrar um break ou até chegar ao fim do switch (incluindo o default, se houver). Esse comportamento tem até nome: é chamado de fall-through. Às vezes ele é usado de propósito (por exemplo, quando vários case diferentes devem levar exatamente à mesma ação), mas na grande maioria das vezes esquecer o break é um erro comum de quem está começando — e o resultado costuma ser bem confuso de depurar, porque o código executa "mais coisa" do que deveria, sem nenhum erro de compilação avisando.
O switch moderno: arrow syntax e switch como expressão
O switch que acabamos de ver (com case, dois-pontos e break) é chamado de switch statement (comando switch): ele executa ações, mas não devolve valor nenhum. Esse foi o único formato disponível por muitos anos, mas a partir do Java 14, ele ganhou uma segunda forma, o switch expression (switch como expressão), capaz de calcular e devolver um valor diretamente, além de resolver de vez o problema do fall-through acidental.
A primeira mudança é de sintaxe: em vez de case valor: seguido de break, você pode escrever case valor ->, com uma seta:
public class Main {
public static void main(String[] args) {
String operacao = "mul";
switch (operacao) {
case "soma" -> System.out.println("Operação: soma");
case "sub" -> System.out.println("Operação: subtração");
case "mul" -> System.out.println("Operação: multiplicação");
case "div" -> System.out.println("Operação: divisão");
default -> System.out.println("Operação inválida!");
}
}
}
Com a seta, não existe mais fall-through: cada case executa apenas o que está do lado direito da seta, e o switch já encerra ali — sem precisar (e sem nem poder) usar break. É uma sintaxe mais enxuta e, ao mesmo tempo, mais segura, já que elimina de raiz o erro clássico de esquecer o break.
A segunda mudança, ainda mais relevante, é que o switch passa a poder ser usado como expressão: ou seja, ele pode aparecer do lado direito de uma atribuição, devolvendo um valor, exatamente como fazem os operadores aritméticos que vimos na Parte 4. Reescrevendo a calculadora original desta forma:
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.print("Qual é o primeiro número? ");
int a = scanner.nextInt();
scanner.nextLine();
System.out.print("Qual é o segundo número? ");
int b = scanner.nextInt();
scanner.nextLine();
System.out.print("Qual operação você quer realizar (soma/sub/mul/div)? ");
String operacao = scanner.nextLine();
String resultado = switch (operacao) {
case "soma" -> String.format("%d + %d = %d", a, b, a + b);
case "sub" -> String.format("%d - %d = %d", a, b, a - b);
case "mul" -> String.format("%d * %d = %d", a, b, a * b);
case "div" -> {
if (b == 0) {
yield "Não é possível dividir por zero!";
}
yield String.format("%d / %d = %d", a, b, a / b);
}
default -> "Operação inválida!";
};
System.out.println(resultado);
scanner.close();
}
}
Repare em dois detalhes novos:
- O
switchinteiro agora é atribuído diretamente à variávelresultado. Cadacase"devolve" um valor para essa variável, em vez de imprimir algo diretamente. - No
case "div", como a lógica precisa de mais de uma linha (oifdob == 0), usamos um bloco{ }com a palavra-chaveyield.yieldfunciona como umreturn, mas específico para blocos de switch expression: ele define qual valor aquelecaseestá devolvendo. Nos demaiscase, com uma linha só, o valor depois da seta já é o valor devolvido implicitamente, sem precisar deyield.
Outra vantagem do switch como expressão: como o compilador agora sabe que o switch precisa sempre devolver um valor, ele passa a exigir que todos os casos possíveis estejam cobertos (o default normalmente resolve isso) — o que ajuda a pegar, em tempo de compilação, um case esquecido que passaria despercebido em um switch statement tradicional.
Múltiplos valores no mesmo case
A sintaxe moderna também permite agrupar vários valores no mesmo case, separados por vírgula, quando eles devem levar exatamente ao mesmo resultado:
public class Main {
public static void main(String[] args) {
int diaDaSemana = 6;
String tipo = switch (diaDaSemana) {
case 1, 2, 3, 4, 5 -> "dia útil";
case 6, 7 -> "fim de semana";
default -> "dia inválido";
};
System.out.println(tipo); // fim de semana
}
}
Isso substitui, de forma bem mais enxuta, o que antes exigiria vários case empilhados de propósito (usando fall-through) só para compartilhar o mesmo bloco de código.
Pattern Matching for Switch (Java 21)
A evolução mais recente, consolidada como funcionalidade estável a partir do Java 21, é o chamado Pattern Matching for Switch. Com ele, o switch deixa de comparar apenas valores exatos (como um número ou uma String) e passa a conseguir testar o tipo de um objeto diretamente em cada case, já extraindo o valor para uma variável tipada:
public class Main {
public static void main(String[] args) {
Object[] valores = { 42, "Olá", 3.14, true, null };
for (Object valor : valores) {
String descricao = switch (valor) {
case Integer i -> "É um inteiro: " + i;
case String s -> "É um texto com " + s.length() + " caracteres";
case Double d -> "É um número decimal: " + d;
case null -> "É um valor nulo";
default -> "Tipo não reconhecido: " + valor;
};
System.out.println(descricao);
}
}
}
Cada case aqui não testa mais um valor fixo, mas sim um padrão de tipo (Integer i, String s, Double d): se valor for daquele tipo, Java já faz o casting automaticamente e disponibiliza o resultado na variável indicada (i, s ou d), pronta para ser usada dentro do próprio case, sem precisar de nenhuma conversão manual como a que vimos na Parte 3. Repare também no case null: antes do Java 21, um switch recebendo null simplesmente lançava uma exceção em tempo de execução; agora é possível tratar esse caso explicitamente, como mais um case normal.
É possível ainda refinar um case de tipo com uma condição extra, usando a palavra-chave when — o que se chama de guarded pattern (padrão com guarda):
public class Main {
public static void main(String[] args) {
Object valor = 15;
String descricao = switch (valor) {
case Integer i when i < 0 -> "Inteiro negativo";
case Integer i when i == 0 -> "Zero";
case Integer i -> "Inteiro positivo";
default -> "Não é um inteiro";
};
System.out.println(descricao); // Inteiro positivo
}
}
O when permite combinar, no mesmo case, tanto a checagem de tipo quanto uma condição adicional sobre o valor já convertido — algo que, antes do Pattern Matching, exigiria um if extra dentro do bloco do case.
Resumindo essa evolução: entre o Java 12 e o Java 21, o switch deixou de ser apenas uma alternativa mais organizada ao if-else encadeado e passou a ser também uma ferramenta para calcular valores (switch expression) e para verificar e desestruturar tipos (pattern matching) de forma direta e segura, com menos código repetido e menos espaço para os erros clássicos do switch tradicional, como o fall-through acidental.
O que é escopo de variável em Java?
Escopo é o período de vida e a visibilidade de uma variável (em outras palavras, é a região do código onde uma variável existe e pode ser acessada).
Vamos retomar o exemplo de condicionais aninhadas visto anteriormente, agora com comentários indicando onde cada variável pode ser acessada:
public class Main {
public static void main(String[] args) {
int idade = 20;
if (idade >= 18 && idade <= 40) {
// idade é acessível aqui
// as booleanas abaixo NÃO são acessíveis fora deste bloco
boolean estudante = true;
boolean associado = false;
if (estudante || associado) {
// idade, estudante e associado são acessíveis aqui
System.out.println("você pode usar o programa");
}
} else {
// idade ainda é acessível aqui
// estudante e associado NÃO são acessíveis aqui
System.out.println("você não pode usar o programa");
}
}
}
estudante e associado foram declaradas dentro do bloco do primeiro if. Isso significa que elas só existem — e só podem ser usadas (dentro desse bloco e de qualquer bloco aninhado dentro dele, como o if (estudante || associado) mais interno). Fora dali (no bloco do else, por exemplo, ou depois do if inteiro), o compilador simplesmente não reconhece essas variáveis; tentar usá-las geraria um erro de compilação. Variáveis com esse comportamento são chamadas de variáveis locais.
Já idade, por ter sido declarada logo no início do método main, é acessível em qualquer ponto dali para frente dentro desse método, incluindo dentro do bloco do if e do bloco do else (porque ambos estão aninhados dentro do escopo onde idade foi declarada).
Fica então a regra geral: uma variável é acessível dentro do bloco onde foi declarada, e em qualquer bloco aninhado dentro dele (mas nunca fora desse bloco). Cada par de chaves { } que você abre no código cria, na prática, um novo nível de escopo.
Quais são os valores padrão das variáveis em Java?
Nós já vimos, lá na Parte 3, que em geral você precisa inicializar uma variável antes de usá-la em Java — do contrário, o compilador acusa erro. Mas existe uma exceção importante a essa regra: variáveis declaradas no nível da classe (ou seja, fora de qualquer método) recebem um valor padrão automaticamente, mesmo sem serem inicializadas explicitamente.
public class Main {
// recebe 0 como valor padrão
static int idade;
public static void main(String[] args) {
System.out.println(idade); // 0
}
}
Repare na diferença: idade, aqui, não está declarada dentro do método main, mas sim diretamente dentro da classe Main, com o modificador static (que vamos explorar com mais calma quando chegarmos em classes e objetos, mais adiante na série). Mesmo sem nenhuma atribuição explícita, o programa imprime 0 sem erro nenhum.
Isso não vale para variáveis locais. Uma variável declarada dentro de um método continua exigindo inicialização manual (se você tentar usá-la sem antes atribuir um valor, o código não compila, exatamente como vimos na Parte 3).
Os valores padrão variam de acordo com o tipo de dado:
Tipo de dadoValor padrãobyte0short0int0long0Lfloat0.0fdouble0.0dchar'\u0000'booleanfalse
E qualquer tipo de referência (como String, que vimos na Parte 5) recebe null como valor padrão nessas mesmas condições — ou seja, um valor que representa "nenhum objeto atribuído ainda".
O que vem a seguir
Neste sétimo artigo (o primeiro já sob o nome Roadmap Java) aprendemos a tomar decisões dentro de um programa Java, seja com if/else/else if, seja com switch-case, entendemos o conceito de escopo de variável e vimos quais valores padrão o compilador atribui automaticamente a variáveis de classe não inicializadas.
Com isso, já temos praticamente todo o vocabulário básico da linguagem: variáveis, tipos, operadores, entrada e saída, e agora também controle de fluxo. Na Parte 8 desta série, vamos dar o próximo passo natural e começar a trabalhar com coleções de dados, com o tópico "Como trabalhar com Arrays em Java", cobrindo criação, ordenação, busca binária, preenchimento, cópia e comparação de arrays.
Até lá!
Este artigo é parte do Roadmap Java, uma série de introdução a Java escrita do zero, com progressão didática própria pensada para quem nunca programou na linguagem antes.
Artigos dessa série na DIO:
- Roadmap ava - Parte 1, Pré-requisitos e seu primeiro programa
- Roadmap Java - Parte 2, Instalando o Java, o IntelliJ IDEA e criando seu primeiro projeto local
- Roadmap Java - Parte 3, Variáveis e tipos primitivos
- Roadmap Java - Parte 4, Operadores
- Roadmap Java - Parte 5, Strings
- Roadmap Java - Parte 6: Entrada e Saída de Dados
- Roadmap Java — Parte 7: Estruturas condicionais, switch-case, escopo e valores padrão


