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.
- 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,ExceptioneRuntimeException. - 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ção | Exceção | |
|---|---|---|
| Quem encontra | O javac, antes de rodar | A JVM, durante a execução |
| O programa chega a rodar? | Não: nenhuma linha é executada | Sim: roda até o ponto do problema |
| Causa típica | Código escrito errado (falta ;, tipo errado) | Código certo, mas um valor inesperado (zero, null, texto no lugar de número) |
| Como aparece | Main.java:5: error: … | Exception in thread "main" … |
| Aula | Lendo Erros do javac | esta |
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:
- A linha 4 rodou:
Primeira nota: 7apareceu. O programa funcionou normalmente até o problema. - A linha 5 lançou a exceção e o programa parou ali. Nem a própria linha 5 terminou (o
printlnnão mostrou nada). - A linha 6 nunca rodou:
Fim do programanão aparece. E o código de saída (o número que o programa devolve ao terminar, mostrado comecho $?) foi1, que significa "terminou com erro". Um programa que termina bem devolve0.
É 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étodomain. Você vai conhecer outras threads em Threads. - java.lang.ArithmeticException
- O tipo da exceção, com o nome completo:
java.langé o pacote eArithmeticException("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
dividirda classeMain, na linha 3 do arquivoMain.java(return a / b;). - at Main.media(Main.java:6)
- Quem chamou o
dividir: o métodomedia, na linha 6 (return dividir(soma, qtd);). - at Main.main(Main.java:10)
- Quem chamou o
media: omain, 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.
- Leia o tipo (o nome que termina em
ExceptionouError). - Leia a mensagem depois dos dois-pontos.
- Desça pelas linhas
ataté a primeira que cita um arquivo seu (Main.java,Conta.java…) e abra essa linha. - 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
| Classe | Nome em português | O que representa |
|---|---|---|
Throwable | "lançável" | A raiz. Só objetos dessa família podem ser lançados com throw e capturados com catch. |
Error | erro | Problemas 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. |
Exception | exceção | Situações que um programa bem feito pode prever e tratar: arquivo que não existe, texto que não é número. |
RuntimeException | exceçã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.
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ção | Exemplo que provoca | Mensagem real |
|---|---|---|
ArithmeticException | 10 / 0 (com int) | / by zero |
ArrayIndexOutOfBoundsException | notas[5] num array de 5 | Index 5 out of bounds for length 5 |
StringIndexOutOfBoundsException | "Ana".charAt(5) | Index 5 out of bounds for length 3 |
NullPointerException | nome.length() com nome = null | Cannot invoke "String.length()" because "<local1>" is null |
NumberFormatException | Integer.parseInt("29 anos") | For input string: "29 anos" |
InputMismatchException | teclado.nextInt() e a pessoa digita vinte | (sem mensagem) |
ClassCastException | (Integer) coisa, com coisa sendo uma String | class 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
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 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.
$ 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
| Termo | O que é |
|---|---|
| Exceção | Objeto 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 tratada | Ninguém capturou: o Java mostra o stack trace e encerra com código de saída 1 |
| Stack trace | O 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 |
Throwable | Raiz de todas as exceções |
Error | Problema grave da JVM; não trate |
Exception | Problema que o programa pode tratar |
RuntimeException | Exceçõ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?
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 primeiraaté 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) eException, que inclui o ramoRuntimeException(erros de lógica). - As mais comuns:
ArithmeticException,ArrayIndexOutOfBoundsException,NullPointerException,NumberFormatException,InputMismatchException. Próximo passo: tratá-las com try e catch.