Java Exceções

Até aqui, quando algo dava errado num programa seu, ele simplesmente parava e despejava na tela umas linhas assustadoras em inglês. Essas linhas têm nome: é uma exceção com o seu stack trace. Nesta aula você vai entender o que é uma exceção, ler cada pedaço daquela mensagem e conhecer a "árvore genealógica" das exceções do Java. Nas próximas aulas, você aprende a tratá-las.

Você vai aprender
  • O que é uma exceção e qual a diferença para um erro de compilação.
  • O que acontece com o programa quando uma exceção não é tratada.
  • Como ler um stack trace linha por linha: o tipo, a mensagem e cada at Classe.metodo(Arquivo.java:linha).
  • Como achar a linha do seu código no meio de um stack trace comprido.
  • A hierarquia: Throwable, Error, Exception e RuntimeException.
  • As exceções que você mais vai encontrar e o que cada uma quer dizer.

Todos os programas desta aula foram rodados na máquina da Ana, com o JDK 25, usando o comando java Main.java, que compila e executa de uma vez (veja javac e java).


A ideia antes do código

Imagine a Ana no caixa de uma padaria. O normal é: o cliente pede, ela cobra, entrega o pão. Mas às vezes acontece algo fora do normal: a máquina de cartão recusa, acabou o troco, o cliente pede um pão que não existe. A Ana não pode simplesmente "continuar" como se nada tivesse acontecido. Ela para o atendimento e avisa: "não tenho troco". Alguém (ela mesma, a gerente) decide o que fazer.

No Java é igual. Uma exceção (do latim exceptio, "o que fica fora da regra"; em inglês, exception) é um aviso de que aconteceu algo fora do normal enquanto o programa rodava: dividir por zero, pegar a posição 10 de um array de 5, transformar "abc" em número. O Java interrompe o que estava fazendo e "lança" o aviso. Se ninguém tratar o aviso, o programa termina ali.

Erro de compilaçãoExceção
Quem encontraO javac, antes de rodarA JVM, durante a execução
O programa chega a rodar?Não: nenhuma linha é executadaSim: roda até o ponto do problema
Causa típicaCódigo escrito errado (falta ;, tipo errado)Código certo, mas um valor inesperado (zero, null, texto no lugar de número)
Como apareceMain.java:5: error: …Exception in thread "main" …
AulaLendo Erros do javacesta

O programa "quebrando"

A Ana guardou as notas de cinco provas num array. As posições de um array começam em 0, então as cinco notas estão nas posições 0, 1, 2, 3 e 4. Veja o que acontece quando o programa pede a posição 5:

public class Main {
    public static void main(String[] args) {
        int[] notas = {7, 9, 6, 10, 8};
        System.out.println("Primeira nota: " + notas[0]);
        System.out.println("Sexta nota: " + notas[5]);
        System.out.println("Fim do programa");
    }
}

O código está escrito corretamente: o javac não reclama de nada. O problema só aparece quando ele roda:

$ java Main.java
Primeira nota: 7
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 5 out of bounds for length 5
	at Main.main(Main.java:5)
$ echo $?
1

Repare em três coisas:

  1. A linha 4 rodou: Primeira nota: 7 apareceu. O programa funcionou normalmente até o problema.
  2. A linha 5 lançou a exceção e o programa parou ali. Nem a própria linha 5 terminou (o println não mostrou nada).
  3. A linha 6 nunca rodou: Fim do programa não aparece. E o código de saída (o número que o programa devolve ao terminar, mostrado com echo $?) foi 1, que significa "terminou com erro". Um programa que termina bem devolve 0.

É isso que chamamos de exceção não tratada (ou "não capturada"): ninguém cuidou do aviso, então o Java mostra o relatório do problema e encerra o programa. Na aula try e catch, você vai aprender a cuidar do aviso e deixar o programa continuar.

Missão: o programa abaixo mostra a primeira nota. Faça-o mostrar a quinta nota (8, na posição 4) e, na linha seguinte, tentar mostrar uma nota que não existe. Rode e leia a exceção.

public class Main {
    public static void main(String[] args) {
        int[] notas = {7, 9, 6, 10, 8};
        System.out.println("Primeira nota: " + notas[0]);
        System.out.println("Fim do programa");
    }
}
public class Main {
    public static void main(String[] args) {
        int[] notas = {7, 9, 6, 10, 8};
        System.out.println("Quinta nota: " + notas[4]);
        System.out.println("Nota inexistente: " + notas[5]);
        System.out.println("Fim do programa");
    }
}

Lendo o stack trace, linha por linha

O "relatório" que o Java mostra se chama stack trace (em inglês, "rastro da pilha"). Parece assustador, mas ele tem sempre as mesmas partes. Vamos usar um programa com três métodos, um chamando o outro, para ver o rastro completo:

public class Main {
    static int dividir(int a, int b) {
        return a / b;
    }
    static double media(int soma, int qtd) {
        return dividir(soma, qtd);
    }
    public static void main(String[] args) {
        System.out.println("Começando");
        System.out.println(media(30, 0));
        System.out.println("Fim");
    }
}

O main chama media(30, 0), que chama dividir(30, 0), que tenta fazer 30 / 0. Divisão de inteiro por zero não existe:

$ java Main.java
Começando
Exception in thread "main" java.lang.ArithmeticException: / by zero
	at Main.dividir(Main.java:3)
	at Main.media(Main.java:6)
	at Main.main(Main.java:10)

Agora, pedaço por pedaço da primeira linha:

Exception in thread "main"
"Exceção na thread main". Uma thread ("linha de execução") é um caminho que o programa está percorrendo; o seu programa começa na thread chamada main, a mesma do método main. Você vai conhecer outras threads em Threads.
java.lang.ArithmeticException
O tipo da exceção, com o nome completo: java.lang é o pacote e ArithmeticException ("exceção aritmética") é a classe. O nome já diz muito: algo deu errado numa conta.
: / by zero
A mensagem, depois dos dois-pontos: "dividido por zero". Nem toda exceção tem mensagem; quando tem, ela costuma dizer o valor que causou o problema.

E cada linha que começa com at ("em") é uma moldura (frame) da pilha de chamadas:

at Main.dividir(Main.java:3)
Onde a exceção nasceu: no método dividir da classe Main, na linha 3 do arquivo Main.java (return a / b;).
at Main.media(Main.java:6)
Quem chamou o dividir: o método media, na linha 6 (return dividir(soma, qtd);).
at Main.main(Main.java:10)
Quem chamou o media: o main, na linha 10. É o fundo da pilha, onde tudo começou.

Por que "pilha"?

Pense numa pilha de pratos. Quando o main chama o media, o Java coloca um prato "media" em cima do prato "main". Quando o media chama o dividir, outro prato vai para o topo. O stack trace é uma foto da pilha no momento da exceção, de cima para baixo:

  ┌───────────────────────────┐
  │ dividir   (linha 3)  ← a exceção nasceu aqui (topo)
  ├───────────────────────────┤
  │ media     (linha 6)  ← chamou o dividir
  ├───────────────────────────┤
  │ main      (linha 10) ← chamou o media (fundo)
  └───────────────────────────┘

Por isso a regra de leitura é: a primeira linha at diz onde aconteceu; as de baixo dizem o caminho que levou até lá. Às vezes o erro "de verdade" não está na linha de cima: aqui, o dividir faz exatamente o que deveria; quem passou o zero foi o main, na linha 10. O stack trace mostra o caminho todo para você decidir onde consertar.

Uma receita para qualquer stack trace
  1. Leia o tipo (o nome que termina em Exception ou Error).
  2. Leia a mensagem depois dos dois-pontos.
  3. Desça pelas linhas at até a primeira que cita um arquivo seu (Main.java, Conta.java…) e abra essa linha.
  4. Se a linha estiver certa, desça mais um nível: quem chamou e com que valores?

Missão: rode o programa e leia o stack trace. Depois conserte a chamada no main para calcular a média de 30 pontos em 2 provas. A saída deve ser 15.0.

public class Main {
    static int dividir(int a, int b) {
        return a / b;
    }
    static double media(int soma, int qtd) {
        return dividir(soma, qtd);
    }
    public static void main(String[] args) {
        System.out.println("Começando");
        System.out.println(media(30, 0));
        System.out.println("Fim");
    }
}
public class Main {
    static int dividir(int a, int b) {
        return a / b;
    }
    static double media(int soma, int qtd) {
        return dividir(soma, qtd);
    }
    public static void main(String[] args) {
        System.out.println("Começando");
        System.out.println(media(30, 2));
        System.out.println("Fim");
    }
}

Stack traces compridos: procure o seu arquivo

Quando a exceção nasce dentro da biblioteca do Java, o rastro mostra também os métodos do próprio Java. A Ana tentou transformar um texto em número com Integer.parseInt (veja Wrappers):

public class Main {
    public static void main(String[] args) {
        String idade = "29 anos";
        int numero = Integer.parseInt(idade);
        System.out.println(numero + 1);
    }
}
$ java Main.java
Exception in thread "main" java.lang.NumberFormatException: For input string: "29 anos"
	at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
	at java.base/java.lang.Integer.parseInt(Integer.java:565)
	at java.base/java.lang.Integer.parseInt(Integer.java:662)
	at Main.main(Main.java:4)

As três primeiras linhas at começam com java.base/: são métodos do módulo java.base, o coração da biblioteca padrão. Você não vai abrir o Integer.java para consertar o Java; o problema está no valor que você passou. A primeira linha sua é at Main.main(Main.java:4): a chamada Integer.parseInt(idade). E a mensagem conta o motivo: For input string: "29 anos", "para o texto de entrada "29 anos"". O texto tem letras, então não vira número.

Às vezes o rastro do Java é bem mais comprido. Este é o charAt pedindo a décima letra de "uva":

$ java Main.java
Primeira letra: u
Exception in thread "main" java.lang.StringIndexOutOfBoundsException: Index 9 out of bounds for length 3
	at java.base/jdk.internal.util.Preconditions$1.apply(Preconditions.java:55)
	at java.base/jdk.internal.util.Preconditions$1.apply(Preconditions.java:52)
	at java.base/jdk.internal.util.Preconditions$4.apply(Preconditions.java:213)
	at java.base/jdk.internal.util.Preconditions$4.apply(Preconditions.java:210)
	at java.base/jdk.internal.util.Preconditions.outOfBounds(Preconditions.java:98)
	at java.base/jdk.internal.util.Preconditions.outOfBoundsCheckIndex(Preconditions.java:106)
	at java.base/jdk.internal.util.Preconditions.checkIndex(Preconditions.java:302)
	at java.base/java.lang.String.checkIndex(String.java:4904)
	at java.base/java.lang.StringLatin1.charAt(StringLatin1.java:45)
	at java.base/java.lang.String.charAt(String.java:1624)
	at Main.main(Main.java:5)

Dez linhas do Java e, lá no fim, uma sua. Não se assuste: pule tudo o que começa com java.base/ e vá direto para Main.java:5. A mensagem Index 9 out of bounds for length 3 ("índice 9 fora dos limites para o tamanho 3") já explica tudo: "uva" tem 3 letras, nas posições 0, 1 e 2.


Exceções são objetos

Uma exceção não é só uma mensagem na tela: é um objeto de verdade, criado a partir de uma classe, como os objetos que você criou em Classes e Objetos. ArithmeticException, NumberFormatException e todas as outras são classes. Quando o problema acontece, o Java cria um objeto dessa classe (com a mensagem e o stack trace guardados dentro) e o "lança".

E, como toda classe, elas formam uma família por herança. Dá para ver a família no JShell, perguntando a cada classe quem é a "mãe" dela (a superclasse) com .class.getSuperclass():

$ jshell
|  Welcome to JShell -- Version 25.0.4.1
|  For an introduction type: /help intro

jshell> ArithmeticException.class.getSuperclass()
$1 ==> class java.lang.RuntimeException

jshell> RuntimeException.class.getSuperclass()
$2 ==> class java.lang.Exception

jshell> Exception.class.getSuperclass()
$3 ==> class java.lang.Throwable

jshell> Throwable.class.getSuperclass()
$4 ==> class java.lang.Object

jshell> NumberFormatException.class.getSuperclass()
$5 ==> class java.lang.IllegalArgumentException

jshell> java.io.IOException.class.getSuperclass()
$6 ==> class java.lang.Exception

jshell> StackOverflowError.class.getSuperclass()
$7 ==> class java.lang.VirtualMachineError

jshell> VirtualMachineError.class.getSuperclass()
$8 ==> class java.lang.Error

jshell> /exit
|  Goodbye

Seguindo as respostas, dá para montar a árvore inteira.

A hierarquia das exceções

Object
 └─ Throwable ................ tudo o que pode ser lançado
     ├─ Error ................ problema grave da JVM: não se trata
     │   └─ VirtualMachineError
     │       ├─ OutOfMemoryError
     │       └─ StackOverflowError
     └─ Exception ............ problema que o programa pode tratar
         ├─ IOException ...... (checked) entrada e saída: arquivos, rede
         │   └─ FileNotFoundException
         └─ RuntimeException . (unchecked) quase sempre, erro de lógica
             ├─ ArithmeticException
             ├─ NullPointerException
             ├─ ClassCastException
             ├─ IllegalArgumentException
             │   └─ NumberFormatException
             ├─ IndexOutOfBoundsException
             │   ├─ ArrayIndexOutOfBoundsException
             │   └─ StringIndexOutOfBoundsException
             └─ NoSuchElementException
                 └─ InputMismatchException
ClasseNome em portuguêsO que representa
Throwable"lançável"A raiz. Só objetos dessa família podem ser lançados com throw e capturados com catch.
ErrorerroProblemas sérios do ambiente: a memória acabou, a pilha de chamadas estourou. O programa normalmente não tem como se recuperar, e você não deve tentar tratá-los.
ExceptionexceçãoSituações que um programa bem feito pode prever e tratar: arquivo que não existe, texto que não é número.
RuntimeExceptionexceção "de tempo de execução"Um ramo de Exception que quase sempre indica um erro de lógica no programa: índice errado, null esquecido, divisão por zero. O compilador não obriga você a tratá-las.

Essa divisão importa por um motivo que você vai ver em Checked × Unchecked: para as exceções do ramo IOException (e outras que herdam de Exception sem passar por RuntimeException), o javac obriga você a tratar ou declarar. Para as do ramo RuntimeException, não.

"Exceção" e "erro" no dia a dia

Na conversa, todo mundo diz "deu erro". No Java, os nomes são técnicos: Error é só o ramo grave da árvore; o resto são Exception. Nesta e nas próximas aulas, "exceção" quer dizer "qualquer coisa da família Throwable", como é comum nos livros.


As exceções mais comuns

Estas são as exceções que quem está começando mais encontra. Todas foram provocadas de verdade no JDK 25; a mensagem é exatamente a que aparece. A lista completa, para consulta, está em Exceções Comuns.

ExceçãoExemplo que provocaMensagem real
ArithmeticException10 / 0 (com int)/ by zero
ArrayIndexOutOfBoundsExceptionnotas[5] num array de 5Index 5 out of bounds for length 5
StringIndexOutOfBoundsException"Ana".charAt(5)Index 5 out of bounds for length 3
NullPointerExceptionnome.length() com nome = nullCannot invoke "String.length()" because "<local1>" is null
NumberFormatExceptionInteger.parseInt("29 anos")For input string: "29 anos"
InputMismatchExceptionteclado.nextInt() e a pessoa digita vinte(sem mensagem)
ClassCastException(Integer) coisa, com coisa sendo uma Stringclass java.lang.String cannot be cast to class java.lang.Integer (…)

NullPointerException: a mais famosa

null quer dizer "nenhum objeto". Uma variável String com null não guarda texto nenhum, nem vazio. Chamar um método nela é pedir algo a ninguém:

public class Main {
    public static void main(String[] args) {
        String nome = null;
        System.out.println(nome.length());
    }
}
$ java Main.java
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "<local1>" is null
	at Main.main(Main.java:4)

A mensagem diz: "não dá para chamar String.length() porque <local1> é nulo". O <local1> é a forma de o Java dizer "a variável local número 1" (a 0 é o args), porque, por padrão, o compilador não guarda os nomes das variáveis no programa compilado. Se você compilar com a opção -g (de debug, depuração), o nome aparece:

$ javac -g Main.java && java Main
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "nome" is null
	at Main.main(Main.java:4)

InputMismatchException: o usuário digitou outra coisa

Com o Scanner, o programa pede um número e a pessoa digita texto:

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner teclado = new Scanner(System.in);
        System.out.print("Sua idade: ");
        int idade = teclado.nextInt();
        System.out.println("Ano que vem: " + (idade + 1));
    }
}
$ echo "vinte" | java Main.java
Sua idade: Exception in thread "main" java.util.InputMismatchException
	at java.base/java.util.Scanner.throwFor(Scanner.java:977)
	at java.base/java.util.Scanner.next(Scanner.java:1632)
	at java.base/java.util.Scanner.nextInt(Scanner.java:2297)
	at java.base/java.util.Scanner.nextInt(Scanner.java:2251)
	at Main.main(Main.java:7)

O echo "vinte" | faz o papel da pessoa digitando vinte. Esta exceção não tem mensagem (nada depois do nome), e o nome basta: "entrada não combina" com o que foi pedido. Repare que o Sua idade: e a exceção ficaram na mesma linha: o print não pula linha, e a exceção foi escrita logo depois.

ClassCastException: conversão de objeto impossível

public class Main {
    public static void main(String[] args) {
        Object coisa = "banana";
        Integer numero = (Integer) coisa;
    }
}
$ java Main.java
Exception in thread "main" java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Integer (java.lang.String and java.lang.Integer are in module java.base of loader 'bootstrap')
	at Main.main(Main.java:4)

"A classe String não pode ser convertida na classe Integer". A parte entre parênteses é um detalhe técnico (de onde cada classe foi carregada) que você pode ignorar. Você vai entender bem esse caso em Polimorfismo.

Missão: provoque uma StringIndexOutOfBoundsException de propósito: depois de mostrar a primeira letra de fruta, peça com charAt uma letra que não existe. Leia o stack trace e ache a linha do seu código.

public class Main {
    public static void main(String[] args) {
        String fruta = "uva";
        System.out.println("Primeira letra: " + fruta.charAt(0));
    }
}
public class Main {
    public static void main(String[] args) {
        String fruta = "uva";
        System.out.println("Primeira letra: " + fruta.charAt(0));
        System.out.println("Décima letra: " + fruta.charAt(9));
    }
}

Quando dá errado

"Mas a linha que o stack trace aponta está certa!"
Exception in thread "main" java.lang.ArithmeticException: / by zero
	at Main.dividir(Main.java:3)
	at Main.media(Main.java:6)
	at Main.main(Main.java:10)

O que aconteceu: a linha de cima mostra onde o problema apareceu, não necessariamente onde ele foi criado. return a / b; está certo; o zero veio de longe.

Como resolver: desça pelas linhas at. Em cada uma, pergunte: "que valores este método recebeu?". Aqui, a linha 10 passa 0 como quantidade de provas. Conserte o valor na origem (ou valide o parâmetro, como você vai aprender em throw e throws).

O stack trace só mostra arquivos do Java

O que aconteceu: o rastro é longo e as primeiras linhas são todas java.base/…. Isso não quer dizer que o Java está com defeito: quase sempre é um valor errado que o seu código entregou para a biblioteca.

Como resolver: procure a primeira linha at que cita um arquivo seu (Main.java:5). É ali que você chamou a biblioteca com o valor problemático.

Divisão por zero que não dá exceção
$ jshell
|  Welcome to JShell -- Version 25.0.4.1
|  For an introduction type: /help intro

jshell> 10 / 0
|  Exception java.lang.ArithmeticException: / by zero
|        at (#1:1)

jshell> 10.0 / 0
$2 ==> Infinity

jshell> /exit
|  Goodbye

O que aconteceu: só a divisão de inteiros (int, long) por zero lança ArithmeticException. Com double, o resultado é o valor especial Infinity ("infinito"), sem exceção nenhuma (veja double e float). Repare também que o JShell mostra exceções num formato um pouco diferente (| Exception, at (#1:1) = "no trecho 1, linha 1").

Como resolver: se o divisor pode ser zero, verifique antes com um if.


Referência

TermoO que é
ExceçãoObjeto que representa algo fora do normal durante a execução
Lançar (throw)Interromper o código e "enviar" a exceção para quem chamou
Capturar / tratar (catch)Receber a exceção e decidir o que fazer (try e catch)
Exceção não tratadaNinguém capturou: o Java mostra o stack trace e encerra com código de saída 1
Stack traceO relatório: tipo: mensagem + uma linha at por método na pilha
at Classe.metodo(Arquivo.java:N)Uma moldura da pilha: método e linha. A de cima é onde a exceção nasceu
java.base/…Moldura dentro da biblioteca padrão do Java
ThrowableRaiz de todas as exceções
ErrorProblema grave da JVM; não trate
ExceptionProblema que o programa pode tratar
RuntimeExceptionExceções de lógica; o compilador não obriga a tratar

Exercícios

1. O tipo da exceção

No stack trace abaixo, qual é o nome da classe da exceção (sem o pacote)?

Exception in thread "main" java.lang.NumberFormatException: For input string: "dez"
	at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
	at java.base/java.lang.Integer.parseInt(Integer.java:565)
	at java.base/java.lang.Integer.parseInt(Integer.java:662)
	at Main.main(Main.java:6)

2. A linha do seu código

No mesmo stack trace, em que linha do Main.java está a chamada que deu problema?

linha

3. A mãe de todas

Complete: a classe que está no topo da hierarquia de exceções (abaixo de Object) é …

4. Um programa mostrou Começando e depois um stack trace de ArithmeticException. O que aconteceu com as linhas que vinham depois da linha do problema?

5. Num stack trace com várias linhas at, qual indica o lugar exato onde a exceção nasceu?

6. NumberFormatException é filha de IllegalArgumentException, que é filha de…

7. Qual destas linhas lança uma exceção?


Resumo

  • Uma exceção é um aviso de que algo fora do normal aconteceu durante a execução. Erros de compilação acontecem antes, no javac.
  • Exceção não tratada: o programa para na linha do problema, mostra o stack trace e termina com código de saída 1.
  • O stack trace tem o tipo, a mensagem e uma linha at Classe.metodo(Arquivo.java:linha) para cada método da pilha. A primeira at é onde nasceu; as de baixo, o caminho até lá.
  • Em rastros longos, pule as linhas java.base/… e procure o seu arquivo.
  • Exceções são objetos de classes da família Throwable: Error (grave, não trate) e Exception, que inclui o ramo RuntimeException (erros de lógica).
  • As mais comuns: ArithmeticException, ArrayIndexOutOfBoundsException, NullPointerException, NumberFormatException, InputMismatchException. Próximo passo: tratá-las com try e catch.