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.

Você vai aprender
  • O que o erro unreported exception …; must be caught or declared to be thrown quer 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):

Este código tem um erro de propósito.
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 IOException não informada". O Files.readString declara, 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 throws no 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.

Capturar ou declarar?

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:

Este código tem um erro de propósito.
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:

Este código tem um erro de propósito.
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 RuntimeException ou por Error: unchecked.
  • Se chega em Exception sem passar por RuntimeException: 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.

CheckedUnchecked
Herda deException (fora do ramo RuntimeException)RuntimeException ou Error
O javac obriga a tratar ou declarar?Sim (unreported exception)Não
RepresentaProblema externo, previsível, fora do seu controleErro de programação (ou, no caso de Error, falha grave da JVM)
O que fazerTer um plano: capturar ou declararCorrigir o código; capturar só quando faz sentido
ExemplosIOException, FileNotFoundException, NoSuchFileException, InterruptedException, SQLExceptionNullPointerException, ArithmeticException, IllegalArgumentException, NumberFormatException, ArrayIndexOutOfBoundsException, IllegalStateException
Os nomes "checked" e "unchecked"

"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çãoEscolhaExemplo
Quem chamou passou um valor errado (defeito no código dele)Unchecked: IllegalArgumentExceptioncadastrar("Bruno", -3)
Chamaram o método na hora erradaUnchecked: IllegalStateExceptionVender com o caixa fechado
Algo externo pode falhar e quem chama consegue reagirChecked (ou deixe a IOException passar com throws)Ler um arquivo que o usuário escolheu
Uma regra do negócio que quem chama precisa tratarChecked própria (ou unchecked, por escolha do projeto)Saldo insuficiente (veja Criar suas Exceções)
Nada que quem chama possa fazerUncheckedArquivo 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 catch pode 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) ou catch (Error e) em programas comuns: você estaria escondendo exatamente os problemas mais graves.

Quando dá errado

"unreported exception X; must be caught or declared to be thrown"

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.

"exception IOException is never thrown in body of corresponding try statement"
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.

Coloquei 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

ItemResumo
CheckedFilhas de Exception fora de RuntimeException; javac exige catch ou throws
UncheckedRuntimeException e filhas, Error e filhas; javac não exige nada
unreported exception X; must be caught or declared to be thrownExceção checked sem tratamento nem declaração
exception X is never thrown in body of corresponding try statementcatch de checked que nada no try lança
IOException (java.io)Checked: entrada e saída (arquivos, rede)
InterruptedExceptionChecked: Thread.sleep e esperas
StackOverflowErrorPilha estourou (recursão sem fim)
OutOfMemoryError: Java heap spaceMemó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:

public static void main(String[] args) {

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 Exception fora de RuntimeException (IOException, InterruptedException, a própria Exception). O javac exige: 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: RuntimeException e filhas. Indicam defeitos no código; o javac nã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. Nunca catch (Throwable e).