Java Checked × Unchecked
Algumas exceções o javac deixa passar sem dizer nada; outras ele exige que você trate ou declare, e se recusa a compilar até você fazer isso. As primeiras se chamam unchecked ("não verificadas") e as segundas, checked ("verificadas"). Saber a diferença acaba com um dos erros de compilação mais comuns do Java e ajuda você a escolher que exceção lançar.
- O que o erro
unreported exception …; must be caught or declared to be thrownquer dizer e as duas formas de resolvê-lo. - Quais exceções são checked e quais são unchecked, olhando a hierarquia.
- A ideia por trás da divisão: problemas "do mundo lá fora" × erros de programação.
- Quando usar cada uma ao lançar as suas exceções.
- Por que
Error(OutOfMemoryError,StackOverflowError) não se trata.
Esta aula usa throw e throws, da aula anterior, e a hierarquia da aula O que são Exceções.
A ideia antes do código
Pense em dois tipos de problema na vida da Ana:
- Coisas que podem acontecer mesmo se ela fizer tudo certo: o ônibus atrasa, a internet cai, o arquivo que o chefe mandou não está na pasta. Ela não controla isso; só pode se preparar (sair mais cedo, ter um plano B). Um bom chefe exige que ela tenha um plano para essas coisas.
- Erros dela mesma: somar errado, digitar o nome do cliente trocado. Para esses, não adianta ter "plano B" em cada linha; o certo é não errar, revisar, corrigir o processo.
O Java separa as exceções do mesmo jeito. As do primeiro tipo são checked: o compilador é o chefe exigente e confere ("checks") se você tem um plano. As do segundo tipo são unchecked: o compilador não confere, porque elas indicam um defeito que deve ser consertado no código, não tratado.
O erro que o javac mostra
A Ana quer mostrar o conteúdo do arquivo de lembretes com Files.readString, que lê um arquivo inteiro como texto (veja Ler e Escrever Arquivos):
import java.nio.file.Files;
import java.nio.file.Path;
public class Main {
public static void main(String[] args) {
String texto = Files.readString(Path.of("Documentos/notas.txt"));
System.out.print(texto);
}
}
$ java Main.java
Main.java:6: error: unreported exception IOException; must be caught or declared to be thrown
String texto = Files.readString(Path.of("Documentos/notas.txt"));
^
1 error
error: compilation failed
- Main.java:6: error:
- Erro de compilação na linha 6. O programa nem rodou: nenhuma linha foi executada.
- unreported exception IOException
- "Exceção
IOExceptionnão informada". OFiles.readStringdeclara, na assinatura dele,throws IOException. O seu código chama esse método, mas não diz o que fazer se a exceção vier. - must be caught
- "precisa ser capturada" (com
try/catch)… - or declared to be thrown
- "…ou declarada para ser lançada" (com
throwsno seu método). - ^
- O acento circunflexo aponta a chamada que pode lançar: o parêntese de
readString(.
O arquivo existe, o código está "certo", mas o compilador não quer saber: arquivos podem sumir, o disco pode falhar, a permissão pode mudar. Uma IOException (exceção de entrada e saída) é o exemplo clássico de problema do "mundo lá fora". O javac exige um plano.
Solução 1: capturar (try/catch)
Se você sabe o que fazer quando a leitura falhar, trate ali mesmo:
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class Main {
public static void main(String[] args) {
try {
String texto = Files.readString(Path.of("Documentos/notas.txt"));
System.out.print(texto);
} catch (IOException e) {
System.out.println("Não consegui ler o arquivo: " + e.getMessage());
}
}
}
Repare no import java.io.IOException;: a classe fica no pacote java.io, que não é importado automaticamente. Rodando na pasta da Ana:
$ java Main.java Comprar café Ligar para o João Estudar Bash Fazer backup das fotos $ sed -i 's/notas.txt/notas2.txt/' Main.java $ java Main.java Não consegui ler o arquivo: Documentos/notas2.txt
O sed trocou o nome do arquivo no código por um que não existe (veja o sed no Guia Bash), e o catch entrou em ação. Repare que a mensagem da NoSuchFileException é só o caminho do arquivo, o que fica estranho sozinho: com exceções de arquivo, mostre também o tipo ("… " + e, como em throw e throws).
Solução 2: declarar (throws)
Se o método não sabe o que fazer, ele passa a responsabilidade adiante declarando throws IOException. No main, isso quer dizer: se a exceção vier, o programa encerra com o stack trace.
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class Main {
public static void main(String[] args) throws IOException {
String texto = Files.readString(Path.of("Documentos/notas.txt"));
System.out.print(texto);
}
}
$ java Main.java Comprar café Ligar para o João Estudar Bash Fazer backup das fotos
Compila e roda. Num método que não é o main, o throws transfere a obrigação para quem chama: ele, por sua vez, vai ter que capturar ou declarar. A exigência "sobe" junto com a exceção, até alguém tratar.
Pergunte: "este método sabe o que fazer se der errado?". Se sim (mostrar um aviso, usar um valor padrão, tentar de novo), capture. Se não (um método que só lê o arquivo não sabe se o programa deve parar ou pedir outro nome), declare e deixe quem chamou decidir.
Outras exceções checked
A IOException não é a única. Esperar um tempo com Thread.sleep (o programa "dorme" alguns milissegundos) pode ser interrompido por outra parte do programa, e por isso declara InterruptedException, também checked:
public class Main {
public static void main(String[] args) {
System.out.println("Esperando 1 segundo...");
Thread.sleep(1000);
System.out.println("Pronto!");
}
}
$ java Main.java
Main.java:4: error: unreported exception InterruptedException; must be caught or declared to be thrown
Thread.sleep(1000);
^
1 error
error: compilation failed
E a própria classe Exception é checked. Se você lançar uma Exception "pura", o compilador cobra do seu método a mesma coisa que cobrou do readString:
public class Main {
static void retirar(int estoque) {
if (estoque == 0) {
throw new Exception("estoque vazio");
}
System.out.println("Retirado. Restam " + (estoque - 1));
}
public static void main(String[] args) {
retirar(5);
retirar(0);
}
}
$ java Main.java
Main.java:4: error: unreported exception Exception; must be caught or declared to be thrown
throw new Exception("estoque vazio");
^
1 error
error: compilation failed
Missão: faça este programa compilar sem trocar o tipo da exceção: declare throws Exception no método retirar e, no main, capture a exceção mostrando Erro: seguido da mensagem.
public class Main {
static void retirar(int estoque) {
if (estoque == 0) {
throw new Exception("estoque vazio");
}
System.out.println("Retirado. Restam " + (estoque - 1));
}
public static void main(String[] args) {
retirar(5);
retirar(0);
}
}
public class Main {
static void retirar(int estoque) throws Exception {
if (estoque == 0) {
throw new Exception("estoque vazio");
}
System.out.println("Retirado. Restam " + (estoque - 1));
}
public static void main(String[] args) {
try {
retirar(5);
retirar(0);
} catch (Exception e) {
System.out.println("Erro: " + e.getMessage());
}
}
}
Unchecked: o compilador não cobra
Agora, um programa que pode lançar NumberFormatException (se o texto não fosse número) e ArrayIndexOutOfBoundsException (se o índice estivesse errado), sem nenhum try nem throws:
public class Main {
public static void main(String[] args) {
int n = Integer.parseInt("42");
int[] notas = new int[3];
System.out.println(n + notas[0]);
}
}
$ java Main.java 42
Compilou sem reclamação. Essas exceções são unchecked: herdam de RuntimeException. Imagine se não fossem: toda divisão (pode ser por zero), todo acesso a array (pode estar fora), toda chamada de método num objeto (pode ser null) precisaria de try/catch. O código ficaria ilegível. Em vez disso, o Java confia que você vai escrever o código certo, e, se não escrever, a exceção aparece na hora de testar e aponta o defeito.
Como saber se é checked ou unchecked
A regra está toda na hierarquia. Siga as "mães" de uma exceção até o topo:
Throwable
├─ Error ......................... UNCHECKED (não trate)
└─ Exception ..................... CHECKED
├─ IOException ............... CHECKED
│ ├─ FileNotFoundException . CHECKED
│ └─ NoSuchFileException ... CHECKED
├─ InterruptedException ...... CHECKED
└─ RuntimeException .......... UNCHECKED
├─ ArithmeticException ... UNCHECKED
├─ NullPointerException .. UNCHECKED
├─ IllegalArgumentException
│ └─ NumberFormatException UNCHECKED
└─ … todas as filhas ..... UNCHECKED
- Se passa por
RuntimeExceptionou porError: unchecked. - Se chega em
Exceptionsem passar porRuntimeException: checked.
Na prática, você também descobre pelo javac (se ele reclamar com unreported exception, é checked) e pela documentação: métodos que lançam exceções checked mostram throws na assinatura, como public static String readString(Path path) throws IOException.
| Checked | Unchecked | |
|---|---|---|
| Herda de | Exception (fora do ramo RuntimeException) | RuntimeException ou Error |
O javac obriga a tratar ou declarar? | Sim (unreported exception) | Não |
| Representa | Problema externo, previsível, fora do seu controle | Erro de programação (ou, no caso de Error, falha grave da JVM) |
| O que fazer | Ter um plano: capturar ou declarar | Corrigir o código; capturar só quando faz sentido |
| Exemplos | IOException, FileNotFoundException, NoSuchFileException, InterruptedException, SQLException | NullPointerException, ArithmeticException, IllegalArgumentException, NumberFormatException, ArrayIndexOutOfBoundsException, IllegalStateException |
"Checked" vem de "checked by the compiler", verificada pelo compilador. Não tem a ver com a exceção acontecer ou não: as duas acontecem durante a execução. A diferença é só o que o javac cobra antes.
Quando usar cada uma (nas suas exceções)
Ao lançar uma exceção com throw, você escolhe o tipo, e a escolha decide se quem chama vai ser obrigado a tratar. Um guia prático:
| Situação | Escolha | Exemplo |
|---|---|---|
| Quem chamou passou um valor errado (defeito no código dele) | Unchecked: IllegalArgumentException | cadastrar("Bruno", -3) |
| Chamaram o método na hora errada | Unchecked: IllegalStateException | Vender com o caixa fechado |
| Algo externo pode falhar e quem chama consegue reagir | Checked (ou deixe a IOException passar com throws) | Ler um arquivo que o usuário escolheu |
| Uma regra do negócio que quem chama precisa tratar | Checked própria (ou unchecked, por escolha do projeto) | Saldo insuficiente (veja Criar suas Exceções) |
| Nada que quem chama possa fazer | Unchecked | Arquivo de configuração interno corrompido |
Muitos programadores e bibliotecas modernas preferem exceções unchecked para quase tudo, porque o throws obrigatório "contamina" todos os métodos do caminho. Outros gostam da segurança do compilador. Os dois estilos existem no mundo real; o importante é ser consistente dentro do mesmo programa. Quando precisar transformar uma checked em unchecked (por exemplo, dentro de um lambda, que não aceita throws), embrulhe-a: throw new UncheckedIOException(e); ou throw new RuntimeException(e);. Esse "embrulho" (a causa) é explicado em Criar suas Exceções.
Missão: o mesmo programa do estoque, agora com outra solução: em vez de tratar ou declarar, troque Exception por uma exceção unchecked adequada, IllegalStateException. O programa vai compilar sem throws nem try, e a exceção vai aparecer na execução.
public class Main {
static void retirar(int estoque) {
if (estoque == 0) {
throw new Exception("estoque vazio");
}
System.out.println("Retirado. Restam " + (estoque - 1));
}
public static void main(String[] args) {
retirar(5);
retirar(0);
}
}
public class Main {
static void retirar(int estoque) {
if (estoque == 0) {
throw new IllegalStateException("estoque vazio");
}
System.out.println("Retirado. Restam " + (estoque - 1));
}
public static void main(String[] args) {
retirar(5);
retirar(0);
}
}
Error: não se trata
O ramo Error da árvore também é unchecked, mas por outro motivo: não é defeito "pequeno" do seu código, é a JVM dizendo que não consegue continuar. Os dois mais conhecidos:
StackOverflowError: a pilha estourou
Cada chamada de método ocupa um "prato" na pilha (lembra do stack trace?). A pilha tem tamanho limitado. Uma recursão sem caso de parada empilha pratos até não caber mais:
public class Main {
static int contar(int n) {
return contar(n + 1);
}
public static void main(String[] args) {
contar(1);
}
}
$ javac Main.java $ java Main 2>&1 | head -4 Exception in thread "main" java.lang.StackOverflowError at Main.contar(Main.java:3) at Main.contar(Main.java:3) at Main.contar(Main.java:3) $ java Main 2>&1 | wc -l 1025
O stack trace é tão grande que usamos o head -4 para ver só o começo (o 2>&1 junta a saída de erro com a normal, para o head receber o stack trace; é o redirecionamento, ensinado no Guia Bash). O wc -l conta as linhas: 1025, ou seja, a linha do título e 1024 linhas at Main.contar. A JVM corta o rastro em 1024 molduras; a pilha real tinha milhares de chamadas, e o main nem aparece.
OutOfMemoryError: a memória acabou
A JVM tem um limite de memória para os objetos (o heap). A opção -Xmx64m limita esse espaço a 64 MB, e o programa pede um array de 50 milhões de long (8 bytes cada, 400 MB):
public class Main {
public static void main(String[] args) {
System.out.println("Pedindo 400 MB de memória...");
long[] enorme = new long[50_000_000];
System.out.println("Consegui: " + enorme.length);
}
}
$ javac Main.java $ java -Xmx64m Main Pedindo 400 MB de memória... Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at Main.main(Main.java:4) $ echo $? 1
"Falta de memória: espaço do heap do Java". O nome diz Error, não Exception, e a primeira linha ainda começa com Exception in thread: esse texto é usado para qualquer Throwable não tratado.
Por que não tratar
- Não há o que fazer: se a memória acabou, até o código do
catchpode não ter memória para rodar. Se a pilha estourou, a JVM está num estado em que quase nada é seguro. - O conserto é no código ou na configuração: a recursão precisa de um caso de parada; o programa precisa usar menos memória ou ganhar mais (
-Xmx). - Por isso, nunca escreva
catch (Throwable e)oucatch (Error e)em programas comuns: você estaria escondendo exatamente os problemas mais graves.
Quando dá errado
O que aconteceu: você chamou um método (ou usou throw) com uma exceção checked e não disse o que fazer com ela.
Como resolver: escolha uma: envolva a linha em try { … } catch (X e) { … }, ou acrescente throws X na assinatura do seu método. Se a exceção é sua e indica erro de programação, considere trocá-la por uma unchecked.
import java.io.IOException;
public class Main {
public static void main(String[] args) {
try {
System.out.println("Olá");
} catch (IOException e) {
System.out.println("Erro de leitura");
}
}
}
$ java Main.java
Main.java:7: error: exception IOException is never thrown in body of corresponding try statement
} catch (IOException e) {
^
1 error
error: compilation failed
O que aconteceu: o contrário do erro anterior. Para exceções checked, o javac sabe exatamente quem pode lançá-las, e nada dentro do try lança IOException: o catch seria código morto. (Com unchecked, como catch (NumberFormatException e), o javac não reclama, porque elas podem surgir de qualquer lugar.)
Como resolver: apague o catch desnecessário, ou confira se a chamada que lança ficou fora do try.
throws Exception em tudo e "resolveu"
O que aconteceu: compila, mas agora todo método que chama esses métodos também precisa tratar ou declarar Exception, que é genérica demais: quem chama não sabe o que pode dar errado.
Como resolver: declare o tipo específico (throws IOException) e trate no ponto em que se sabe o que fazer.
Referência
| Item | Resumo |
|---|---|
| Checked | Filhas de Exception fora de RuntimeException; javac exige catch ou throws |
| Unchecked | RuntimeException e filhas, Error e filhas; javac não exige nada |
unreported exception X; must be caught or declared to be thrown | Exceção checked sem tratamento nem declaração |
exception X is never thrown in body of corresponding try statement | catch de checked que nada no try lança |
IOException (java.io) | Checked: entrada e saída (arquivos, rede) |
InterruptedException | Checked: Thread.sleep e esperas |
StackOverflowError | Pilha estourou (recursão sem fim) |
OutOfMemoryError: Java heap space | Memória dos objetos acabou (-Xmx define o limite) |
throw new UncheckedIOException(e) | Embrulha uma IOException numa unchecked |
Exercícios
1. A classe que separa
Toda exceção que herda desta classe é unchecked (complete o nome):
2. Declarar
Complete para o main deixar a InterruptedException do Thread.sleep passar:
3. Checked ou unchecked?
FileNotFoundException é filha de IOException. Ela é checked ou unchecked?
4. Qual destas exceções o javac obriga você a tratar ou declarar?
5. O programa terminou com java.lang.StackOverflowError. Qual a melhor atitude?
6. Um método seu recebe uma porcentagem e ela veio como 150. Que exceção lançar?
Resumo
- Checked: filhas de
Exceptionfora deRuntimeException(IOException,InterruptedException, a própriaException). Ojavacexige:unreported exception …; must be caught or declared to be thrown. - Duas saídas: capturar (
try/catch) se o método sabe o que fazer; declarar (throws) se não sabe. - Unchecked:
RuntimeExceptione filhas. Indicam defeitos no código; ojavacnão cobra nada. - Nas suas exceções: valor ou estado errado → unchecked (
IllegalArgumentException,IllegalStateException); problema externo que quem chama pode resolver → checked. Error(StackOverflowError,OutOfMemoryError) é unchecked e não se trata: conserte o código ou a configuração. Nuncacatch (Throwable e).