Java try e catch

Na aula O que são Exceções, você viu o programa "quebrar": uma exceção não tratada encerra tudo. Agora você vai aprender a tratar a exceção: dizer ao Java "tente fazer isto; se der este problema, faça aquilo" e deixar o programa seguir em frente. As palavras para isso são try e catch.

Você vai aprender
  • Escrever um bloco try/catch e entender o caminho que o programa percorre.
  • Capturar uma exceção específica e o que acontece com as outras.
  • Usar vários catch, e por que a ordem entre eles importa.
  • Juntar exceções num catch só com o multi-catch (|).
  • Tirar informação da exceção: getMessage(), toString() e printStackTrace().
  • Por que um catch vazio ("engolir a exceção") é um dos piores hábitos em Java.

A ideia antes do código

Pense num trapezista de circo. Ele tenta o salto. Quase sempre dá certo. Mas, se ele cair, há uma rede de proteção embaixo que o pega, e o espetáculo continua. Sem a rede, uma queda encerra o show.

No Java:

  • try ("tente") é o salto: o código que pode dar problema.
  • catch ("pegue", "capture") é a rede: o que fazer se uma exceção de certo tipo for lançada dentro do try.

O primeiro try/catch

A Ana recebe idades como texto (de um formulário, de um arquivo) e precisa transformá-las em número com Integer.parseInt. Se o texto não for um número, o parseInt lança NumberFormatException. Desta vez, o programa vai estar preparado:

public class Main {
    public static void main(String[] args) {
        String texto = "29 anos";
        try {
            int idade = Integer.parseInt(texto);
            System.out.println("Idade: " + idade);
        } catch (NumberFormatException e) {
            System.out.println("Isso não é um número: " + texto);
        }
        System.out.println("Fim do programa");
    }
}
try {
Abre o bloco protegido. Tudo entre estas chaves roda normalmente, mas "vigiado".
int idade = Integer.parseInt(texto);
A linha que pode lançar a exceção.
System.out.println("Idade: " + idade);
Só roda se a linha de cima deu certo.
} catch (
Fecha o try e abre a "rede". Os parênteses dizem que tipo de exceção esta rede pega.
NumberFormatException e
O tipo da exceção e um nome de variável para ela. Por costume, o nome é e (de exception), mas pode ser qualquer um. Dentro do catch, e é o objeto da exceção, com a mensagem e o stack trace.
{ … }
O que fazer quando a exceção acontece: aqui, avisar com uma mensagem amigável.
$ java Main.java
Isso não é um número: 29 anos
Fim do programa
$ echo $?
0

Compare com a aula anterior: nada de stack trace, o Fim do programa apareceu e o código de saída é 0 ("terminou bem"). A exceção aconteceu do mesmo jeito, mas foi tratada.

O caminho do programa

Repare que Idade: … não apareceu. Quando uma linha dentro do try lança uma exceção, o Java abandona o resto do try e pula direto para o catch. Depois do catch, segue com o que vem depois do bloco todo. Os dois caminhos possíveis:

LinhaCom "29 anos" (exceção)Com "29" (tudo certo)
5  parseInt(texto)lança NumberFormatExceptionroda: idade = 29
6  println("Idade: …")puladaroda
8  println("Isso não é…")rodapulada (sem exceção, o catch não roda)
10 println("Fim…")rodaroda

Trocando "29 anos" por "29", a saída real é:

$ java Main.java
Idade: 29
Fim do programa

Missão: rode o programa e veja-o quebrar. Depois proteja as duas linhas de dentro com try e um catch (NumberFormatException e) que mostre Isso não é um número. O Fim do programa deve aparecer.

public class Main {
    public static void main(String[] args) {
        String texto = "29 anos";
        int idade = Integer.parseInt(texto);
        System.out.println("Idade: " + idade);
        System.out.println("Fim do programa");
    }
}
public class Main {
    public static void main(String[] args) {
        String texto = "29 anos";
        try {
            int idade = Integer.parseInt(texto);
            System.out.println("Idade: " + idade);
        } catch (NumberFormatException e) {
            System.out.println("Isso não é um número: " + texto);
        }
        System.out.println("Fim do programa");
    }
}

O catch só pega o tipo que você pediu

A rede do catch tem formato: catch (NumberFormatException e) pega só NumberFormatException (e as classes filhas dela). Se outra exceção acontecer no try, ela passa direto pela rede:

public class Main {
    public static void main(String[] args) {
        int soma = 22;
        int provas = 0;
        try {
            System.out.println("Média: " + soma / provas);
        } catch (NumberFormatException e) {
            System.out.println("Número inválido");
        }
        System.out.println("Fim do programa");
    }
}
$ java Main.java
Exception in thread "main" java.lang.ArithmeticException: / by zero
	at Main.main(Main.java:6)

A divisão lançou ArithmeticException, que não é uma NumberFormatException. O catch foi ignorado, a exceção continuou "caindo" e encerrou o programa, como se o try nem existisse.

Pegar a classe mãe pega as filhas

Lembra da hierarquia? ArithmeticException e NumberFormatException são filhas de RuntimeException. Um catch da classe mãe pega qualquer filha:

        } catch (RuntimeException e) {
            System.out.println("Problema: " + e);
        }
$ java Main.java
Problema: java.lang.ArithmeticException: / by zero
Fim do programa

Subindo mais, catch (Exception e) pega quase tudo. Parece prático, mas é perigoso: você passa a tratar do mesmo jeito problemas muito diferentes, inclusive os que você nem imaginava (um NullPointerException causado por um defeito no seu código, por exemplo). A regra dos programadores experientes: capture a exceção mais específica que você sabe tratar.


Vários catch

Um try pode ter vários catch, um para cada tipo de problema, cada um com sua resposta. O Java testa os catch de cima para baixo e usa o primeiro que combinar:

public class Main {
    public static void main(String[] args) {
        String[] textos = {"10", "abc", "0"};
        for (String t : textos) {
            try {
                int n = Integer.parseInt(t);
                System.out.println("100 / " + n + " = " + (100 / n));
            } catch (NumberFormatException e) {
                System.out.println("\"" + t + "\" não é número");
            } catch (ArithmeticException e) {
                System.out.println("Não dá para dividir por zero");
            }
        }
    }
}
$ java Main.java
100 / 10 = 10
"abc" não é número
Não dá para dividir por zero

O for-each passa pelos três textos. Cada volta tem o seu próprio try: "10" dá certo; "abc" cai no primeiro catch; "0" passa pelo parseInt, mas a divisão cai no segundo. E, como o try está dentro do laço, um problema numa volta não impede as outras.

A ordem importa

Como o Java usa o primeiro catch que combina, um catch da classe mãe antes de uma filha deixaria a filha inalcançável. O javac percebe isso e recusa o programa:

Este código tem um erro de propósito.
public class Main {
    public static void main(String[] args) {
        String texto = "abc";
        try {
            int n = Integer.parseInt(texto);
            System.out.println(100 / n);
        } catch (Exception e) {
            System.out.println("Algo deu errado");
        } catch (NumberFormatException e) {
            System.out.println("Não é número");
        }
    }
}
$ java Main.java
Main.java:9: error: exception NumberFormatException has already been caught
        } catch (NumberFormatException e) {
          ^
1 error
error: compilation failed

"A exceção NumberFormatException já foi capturada": o catch (Exception e) da linha 7 já pega qualquer NumberFormatException, então o da linha 9 nunca seria usado. A última linha, error: compilation failed ("a compilação falhou"), é do java Main.java avisando que nem chegou a rodar. Regra: do mais específico (filha) para o mais geral (mãe). Inverter os dois catch resolve.

Missão: o programa trata texto inválido, mas quebra no "0". Acrescente um segundo catch, para ArithmeticException, que mostre Não dá para dividir por zero, e faça o Fim aparecer.

public class Main {
    public static void main(String[] args) {
        String[] textos = {"10", "abc", "0"};
        for (String t : textos) {
            try {
                int n = Integer.parseInt(t);
                System.out.println("100 / " + n + " = " + (100 / n));
            } catch (NumberFormatException e) {
                System.out.println("\"" + t + "\" não é número");
            }
        }
        System.out.println("Fim");
    }
}
public class Main {
    public static void main(String[] args) {
        String[] textos = {"10", "abc", "0"};
        for (String t : textos) {
            try {
                int n = Integer.parseInt(t);
                System.out.println("100 / " + n + " = " + (100 / n));
            } catch (NumberFormatException e) {
                System.out.println("\"" + t + "\" não é número");
            } catch (ArithmeticException e) {
                System.out.println("Não dá para dividir por zero");
            }
        }
        System.out.println("Fim");
    }
}

Multi-catch: vários tipos num catch só Java 7+

Quando a resposta é a mesma para dois ou mais tipos, você pode juntá-los com uma barra vertical |, que aqui se lê "ou":

        } catch (NumberFormatException | ArithmeticException e) {
            System.out.println("Entrada inválida: " + e.getMessage());
        }
NumberFormatException | ArithmeticException
"Se for uma NumberFormatException ou uma ArithmeticException…".
e
Uma variável só, que pode guardar qualquer um dos tipos. Você pode chamar nela os métodos comuns a todas as exceções, como getMessage().

Um programa completo, que divide a conta do restaurante entre os amigos da Ana (ele lê do teclado com o Scanner):

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner teclado = new Scanner(System.in);
        System.out.print("Total da conta: ");
        String total = teclado.nextLine();
        System.out.print("Pessoas: ");
        String pessoas = teclado.nextLine();
        try {
            int cada = Integer.parseInt(total) / Integer.parseInt(pessoas);
            System.out.println("Cada um paga " + cada);
        } catch (NumberFormatException | ArithmeticException e) {
            System.out.println("Entrada inválida: " + e.getMessage());
        }
    }
}
$ java Main.java
Total da conta: 90
Pessoas: 0
Entrada inválida: / by zero
$ java Main.java
Total da conta: noventa
Pessoas: 3
Entrada inválida: For input string: "noventa"

Não dá para juntar no multi-catch dois tipos em que um é filho do outro: seria redundante, e o javac recusa.

Este código tem um erro de propósito.
        } catch (NumberFormatException | RuntimeException e) {
$ java Main.java
Main.java:7: error: Alternatives in a multi-catch statement cannot be related by subclassing
        } catch (NumberFormatException | RuntimeException e) {
                                         ^
  Alternative NumberFormatException is a subclass of alternative RuntimeException
1 error
error: compilation failed

"As alternativas de um multi-catch não podem ter relação de herança. A alternativa NumberFormatException é subclasse da alternativa RuntimeException." Deixe só a mais geral (RuntimeException) ou troque por tipos sem parentesco.


O que fazer com a exceção capturada

A variável e é um objeto, e tem métodos úteis. Veja todos lado a lado:

public class Main {
    public static void main(String[] args) {
        try {
            int n = Integer.parseInt("12x");
        } catch (NumberFormatException e) {
            System.out.println("getMessage(): " + e.getMessage());
            System.out.println("toString():   " + e);
            System.out.println("Só o nome:    " + e.getClass().getSimpleName());
            System.out.println("--- printStackTrace() ---");
            e.printStackTrace();
            System.out.println("--- o programa continua ---");
        }
    }
}
$ javac Main.java
$ java Main
getMessage(): For input string: "12x"
toString():   java.lang.NumberFormatException: For input string: "12x"
Só o nome:    NumberFormatException
--- printStackTrace() ---
java.lang.NumberFormatException: For input string: "12x"
	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)
--- o programa continua ---
MétodoDevolve / fazQuando usar
e.getMessage()Só a mensagem: For input string: "12x". Pode ser null (exceções sem mensagem, como InputMismatchException).Para mostrar ao usuário um detalhe do problema.
e.toString() ou "" + eNome completo da classe + mensagem.Para registrar o problema numa linha.
e.getClass().getSimpleName()Só o nome da classe, sem o pacote.Mensagens curtas.
e.printStackTrace()Escreve o stack trace completo na saída de erro (System.err), sem encerrar o programa.Enquanto você está desenvolvendo, para saber exatamente onde foi.

Repare: o printStackTrace() mostra o mesmo relatório de uma exceção não tratada, só que sem o Exception in thread "main", e o programa continua.

Por que javac + java aqui?

Rodando com java Main.java, o printStackTrace() mostra, no fim, mais cinco linhas at do próprio lançador (jdk.internal.reflect…, SourceLauncher…), que é quem compila e chama o seu main nesse modo. Elas não fazem parte do seu programa. Com javac e depois java Main, o rastro fica limpo.


Um uso clássico: pedir de novo até acertar

Juntando try/catch com um while, o programa insiste até a pessoa digitar um número válido:

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner teclado = new Scanner(System.in);
        int idade = -1;
        while (idade < 0) {
            System.out.print("Sua idade: ");
            String linha = teclado.nextLine();
            try {
                idade = Integer.parseInt(linha);
            } catch (NumberFormatException e) {
                System.out.println("\"" + linha + "\" não é um número. Tente de novo.");
            }
        }
        System.out.println("Ano que vem você terá " + (idade + 1) + " anos.");
    }
}
$ java Main.java
Sua idade: vinte
"vinte" não é um número. Tente de novo.
Sua idade: 29
Ano que vem você terá 30 anos.

A idade começa em -1 (um valor impossível) para o laço rodar pelo menos uma vez. Se o parseInt falhar, a atribuição idade = … não acontece, a idade continua -1 e o laço repete. Repare também que o programa lê a linha inteira com nextLine() e converte com parseInt, em vez de usar nextInt(): assim o texto inválido não fica "preso" no Scanner.


Não engula exceções

"Engolir" uma exceção é capturá-la e não fazer nada. O javac aceita, o programa não quebra… e é exatamente esse o problema. A Ana somou os preços de uma lista:

Este código esconde um problema de propósito.
public class Main {
    public static void main(String[] args) {
        String[] precos = {"10", "25", "7,50", "8"};
        int total = 0;
        for (String p : precos) {
            try {
                total += Integer.parseInt(p);
            } catch (NumberFormatException e) {
            }
        }
        System.out.println("Total: " + total);
    }
}
$ java Main.java
Total: 43

O total certo seria 50,50. O "7,50" lançou NumberFormatException, o catch vazio "engoliu", e o programa mostrou um número errado sem avisar ninguém. Um programa que quebra é chato; um programa que dá a resposta errada em silêncio é muito pior, porque ninguém percebe.

Se você capturou, faça pelo menos uma destas coisas:

  • Avise: System.out.println("Preço inválido ignorado: " + p);
  • Corrija: use um valor padrão que faça sentido, ou peça de novo.
  • Registre: e.printStackTrace(); enquanto desenvolve.
  • Não capture: se você não sabe o que fazer com a exceção ali, deixe-a subir para quem sabe (veja throw e throws).

Formas diferentes, qual usar

SituaçãoForma
Um tipo de problema, uma respostatry { … } catch (TipoException e) { … }
Vários problemas, respostas diferentesVários catch, do mais específico ao mais geral
Vários problemas, a mesma respostaMulti-catch: catch (A | B e)
Família inteira de problemascatch da classe mãe (com cuidado)
Algo que precisa rodar com ou sem exceçãofinally ou try-with-resources (próxima aula)
Você não sabe tratar aliNão capture; deixe subir (throws)
Dá para evitar com um ifPrefira o if: if (provas != 0) é mais claro que capturar ArithmeticException

Quando dá errado

"cannot find symbol" com uma variável criada no try
public class Main {
    public static void main(String[] args) {
        try {
            int idade = Integer.parseInt("29");
        } catch (NumberFormatException e) {
            System.out.println("Não é número");
        }
        System.out.println("Idade: " + idade);
    }
}
$ java Main.java
Main.java:8: error: cannot find symbol
        System.out.println("Idade: " + idade);
                                       ^
  symbol:   variable idade
  location: class Main
1 error
error: compilation failed

O que aconteceu: as chaves do try são um bloco, e uma variável criada dentro de um bloco só existe lá dentro (veja Escopo). Na linha 8, idade já não existe.

Como resolver: declare a variável antes do try, com um valor inicial (int idade = 0;), e só faça a atribuição dentro dele (idade = Integer.parseInt("29");). Ou coloque o println dentro do try.

"exception … has already been caught"

O que aconteceu: um catch de uma classe mãe (Exception, RuntimeException) veio antes do catch de uma filha. O de baixo nunca seria usado.

Como resolver: coloque os catch do mais específico para o mais geral.

O catch existe, mas o programa quebrou mesmo assim

O que aconteceu: a exceção lançada não é do tipo do catch (nem filha dele). Leia a primeira linha do stack trace: o tipo que aparece ali é o que você precisa capturar. Também pode ser que a linha que lançou esteja fora do try: confira o número da linha.

Como resolver: acrescente um catch para esse tipo, ou mova a linha para dentro do try.

O programa não quebra, mas o resultado está errado

O que aconteceu: provavelmente há um catch vazio engolindo exceções. Procure por catch (…) { } no código.

Como resolver: coloque pelo menos um aviso ou e.printStackTrace() dentro dele e rode de novo.


Referência

SintaxeO que faz
try { … }Bloco vigiado: se algo lança exceção, o resto dele é pulado
catch (Tipo e) { … }Roda se o try lançou Tipo ou uma filha dele
catch (A e) { … } catch (B e) { … }Vários tratamentos; o primeiro que combina é usado
catch (A | B e) { … }Multi-catch: o mesmo tratamento para A ou B (sem parentesco)
e.getMessage()A mensagem (pode ser null)
e.toString()pacote.Classe: mensagem
e.printStackTrace()Mostra o stack trace em System.err e continua
error: exception X has already been caughtcatch de filha depois do da mãe
error: Alternatives in a multi-catch statement cannot be related by subclassingMulti-catch com mãe e filha juntas

Exercícios

1. A palavra que falta

Complete para capturar o problema do parseInt:

try { n = Integer.parseInt(t); } (NumberFormatException e) { … }

2. Só a mensagem

Complete para mostrar só a mensagem da exceção (sem o nome da classe):

System.out.println(e.);

3. Multi-catch

Complete com o símbolo que junta os dois tipos no mesmo catch:

catch (NumberFormatException ArithmeticException e)

4. O que este trecho mostra?

try {
    System.out.println("A");
    int x = Integer.parseInt("x");
    System.out.println("B");
} catch (NumberFormatException e) {
    System.out.println("C");
}
System.out.println("D");

5. Um try lança ArithmeticException e o único catch é catch (NumberFormatException e). O que acontece?

6. Qual ordem de catch compila?

Missão final: o programa divide a conta, mas quebra com 0 pessoas (e quebraria com texto). Proteja a conta com try e um multi-catch de NumberFormatException e ArithmeticException que mostre Entrada inválida: seguido de e.getMessage(). Teste com 90 e 0.

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner teclado = new Scanner(System.in);
        System.out.print("Total da conta: ");
        String total = teclado.nextLine();
        System.out.print("Pessoas: ");
        String pessoas = teclado.nextLine();
        int cada = Integer.parseInt(total) / Integer.parseInt(pessoas);
        System.out.println("Cada um paga " + cada);
    }
}
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner teclado = new Scanner(System.in);
        System.out.print("Total da conta: ");
        String total = teclado.nextLine();
        System.out.print("Pessoas: ");
        String pessoas = teclado.nextLine();
        try {
            int cada = Integer.parseInt(total) / Integer.parseInt(pessoas);
            System.out.println("Cada um paga " + cada);
        } catch (NumberFormatException | ArithmeticException e) {
            System.out.println("Entrada inválida: " + e.getMessage());
        }
    }
}

Resumo

  • try marca o código que pode lançar exceção; catch (Tipo e) diz o que fazer se ela vier.
  • Quando uma linha do try lança, o resto do try é pulado, o catch roda e o programa continua depois do bloco.
  • O catch pega o tipo pedido e as classes filhas; outros tipos passam direto.
  • Vários catch: do mais específico ao mais geral, senão exception … has already been caught.
  • Multi-catch catch (A | B e): mesma resposta para tipos sem parentesco.
  • getMessage() dá a mensagem; printStackTrace() mostra o rastro completo sem encerrar o programa.
  • Nunca deixe um catch vazio: avise, corrija, registre ou não capture.