Tratamento de exceções em Java

Tratamento de exceções em Java

1. Visão geral

Neste tutorial, passaremos pelos fundamentos do tratamento de exceções em Java, bem como algumas de suas dicas.

2. Primeiros Princípios

2.1. O que é isso?

Para entender melhor as exceções e o tratamento de exceções, vamos fazer uma comparação na vida real.

Imagine que pedimos um produto online, mas durante o trajeto, ocorre uma falha na entrega. Uma boa empresa pode lidar com esse problema e redirecionar normalmente nosso pacote para que ele chegue a tempo.

Da mesma forma, em Java, o código pode apresentar erros ao executar nossas instruções. Bomexception handling pode lidar com erros e redirecionar o programa para dar ao usuário uma experiência positiva.

2.2. Por que usar isso?

Geralmente, escrevemos código em um ambiente idealizado: o sistema de arquivos sempre contém nossos arquivos, a rede está íntegra e a JVM sempre tem memória suficiente. Às vezes chamamos isso de "caminho feliz".

Na produção, no entanto, os sistemas de arquivos podem corromper, as redes quebram e as JVMs ficam sem memória. O bem-estar do nosso código depende de como ele lida com "caminhos infelizes".

Devemos lidar com essas condições porque elas afetam o fluxo do aplicativo negativamente e formamexceptions:

public static List getPlayers() throws IOException {
    Path path = Paths.get("players.dat");
    List players = Files.readAllLines(path);

    return players.stream()
      .map(Player::new)
      .collect(Collectors.toList());
}

Este código opta por não lidar comIOException, passando-o para a pilha de chamadas. Em um ambiente idealizado, o código funciona bem.

Mas o que pode acontecer na produção seplayers.dat  estiver faltando?

Exception in thread "main" java.nio.file.NoSuchFileException: players.dat <-- players.dat file doesn't exist
    at sun.nio.fs.WindowsException.translateToIOException(Unknown Source)
    at sun.nio.fs.WindowsException.rethrowAsIOException(Unknown Source)
    // ... more stack trace
    at java.nio.file.Files.readAllLines(Unknown Source)
    at java.nio.file.Files.readAllLines(Unknown Source)
    at Exceptions.getPlayers(Exceptions.java:12) <-- Exception arises in getPlayers() method, on line 12
    at Exceptions.main(Exceptions.java:19) <-- getPlayers() is called by main(), on line 19

Without handling this exception, an otherwise healthy program may stop running altogether! Precisamos ter certeza de que nosso código tem um plano para quando as coisas derem errado.

Observe também mais um benefício aqui, com exceções, e esse é o próprio rastreamento de pilha. Devido a esse rastreamento de pilha, geralmente podemos identificar o código incorreto sem precisar anexar um depurador.

3. Hierarquia de exceção

Em última análise,exceptions  são apenas objetos Java, com todos eles estendendo-se deThrowable:

              ---> Throwable <---
              |    (checked)     |
              |                  |
              |                  |
      ---> Exception           Error
      |    (checked)        (unchecked)
      |
RuntimeException
  (unchecked)

Existem três categorias principais de condições excepcionais:

  • Exceções verificadas

  • Exceções não verificadas / Exceções de tempo de execução

  • Erros

O tempo de execução e as exceções não verificadas referem-se à mesma coisa. Muitas vezes podemos usá-los de forma intercambiável.

3.1. Checked Exceptions

Exceções verificadas são exceções que o compilador Java exige que tratemos. Temos que declarativamente lançar a exceção na pilha de chamadas ou temos que lidar com isso sozinhos. Mais sobre os dois em um momento.

Oracle’s documentation nos diz para usar exceções verificadas quando podemos esperar que o chamador de nosso método seja capaz de se recuperar.

Alguns exemplos de exceções verificadas sãoIOExceptioneServletException.

3.2. Exceções não verificadas

As exceções não verificadas são exceções que o compilador Javanot exige que tratemos.

Simplificando, se criarmos uma exceção que estendeRuntimeException, ela será desmarcada; caso contrário, será verificado.

E embora isso pareça conveniente,Oracle’s documentation nos diz que há boas razões para ambos os conceitos, como diferenciar entre um erro situacional (marcado) e um erro de uso (não marcado).

Alguns exemplos de exceções não verificadas sãoNullPointerException, IllegalArgumentException, andSecurityException.

3.3. Erros

Erros representam condições sérias e geralmente irrecuperáveis, como incompatibilidade de biblioteca, recursão infinita ou vazamento de memória.

E mesmo que eles não estendamRuntimeException, eles também estão desmarcados.

Na maioria dos casos, seria estranho para nós manipular, instanciar ou estenderErrors. Normalmente, queremos que eles se propaguem até o fim.

Alguns exemplos de erros sãoStackOverflowErroreOutOfMemoryError.

4. Tratamento de exceções

Na API Java, há muitos lugares onde as coisas podem dar errado, e alguns desses locais são marcados com exceções, na assinatura ou no Javadoc:

/**
 * @exception FileNotFoundException ...
 */
public Scanner(String fileName) throws FileNotFoundException {
   // ...
}

Conforme declarado um pouco antes, quando chamamos esses métodos “arriscados”, nósmust tratamos as exceções verificadas emay tratamos as não verificadas. Java nos fornece várias maneiras de fazer isso:

4.1. throws

A maneira mais simples de "manipular" uma exceção é reproduzi-la novamente:

public int getPlayerScore(String playerFile)
  throws FileNotFoundException {

    Scanner contents = new Scanner(new File(playerFile));
    return Integer.parseInt(contents.nextLine());
}

ComoFileNotFoundException  é uma exceção verificada, esta é a maneira mais simples de satisfazer o compilador, masit does mean that anyone that calls our method now needs to handle it too!

parseInt pode lançar umNumberFormatException, mas como está desmarcado, não somos obrigados a lidar com isso.

4.2. try -catch

Se quisermos tentar lidar com a exceção nós mesmos, podemos usar um blocotry-catch. Podemos lidar com isso, reavaliando nossa exceção:

public int getPlayerScore(String playerFile) {
    try {
        Scanner contents = new Scanner(new File(playerFile));
        return Integer.parseInt(contents.nextLine());
    } catch (FileNotFoundException noFile) {
        throw new IllegalArgumentException("File not found");
    }
}

Ou executando etapas de recuperação:

public int getPlayerScore(String playerFile) {
    try {
        Scanner contents = new Scanner(new File(playerFile));
        return Integer.parseInt(contents.nextLine());
    } catch ( FileNotFoundException noFile ) {
        logger.warn("File not found, resetting score.");
        return 0;
    }
}

4.3. finally

Agora, há momentos em que temos código que precisa ser executado independentemente de ocorrer uma exceção, e é aqui que a palavra-chavefinally entra.

Em nossos exemplos até agora, houve um bug desagradável à espreita nas sombras, que é que o Java, por padrão, não retorna identificadores de arquivo para o sistema operacional.

Certamente, se podemos ler o arquivo ou não, queremos ter certeza de que fazemos a limpeza apropriada!

Vamos tentar primeiro da maneira “preguiçosa”:

public int getPlayerScore(String playerFile)
  throws FileNotFoundException {
    Scanner contents = null;
    try {
        contents = new Scanner(new File(playerFile));
        return Integer.parseInt(contents.nextLine());
    } finally {
        if (contents != null) {
            contents.close();
        }
    }
}

Aqui, o blocofinally indica qual código queremos que o Java execute, independentemente do que acontece ao tentar ler o arquivo.

Mesmo seFileNotFoundException for lançado na pilha de chamadas, o Java chamará o conteúdo definally antes de fazer isso.

Nós também podemos lidar com a exceçãoand para garantir que nossos recursos sejam fechados:

public int getPlayerScore(String playerFile) {
    Scanner contents;
    try {
        contents = new Scanner(new File(playerFile));
        return Integer.parseInt(contents.nextLine());
    } catch (FileNotFoundException noFile ) {
        logger.warn("File not found, resetting score.");
        return 0;
    } finally {
        try {
            if (contents != null) {
                contents.close();
            }
        } catch (IOException io) {
            logger.error("Couldn't close the reader!", io);
        }
    }
}

Comoclose também é um método “arriscado”, também precisamos capturar sua exceção!

Isso pode parecer bastante complicado, mas precisamos de cada peça para lidar com cada problema em potencial que possa surgir corretamente.

4.4. try-com-recursos

Felizmente, a partir do Java 7, podemos simplificar a sintaxe acima ao trabalhar com coisas que estendemAutoCloseable:

public int getPlayerScore(String playerFile) {
    try (Scanner contents = new Scanner(new File(playerFile))) {
      return Integer.parseInt(contents.nextLine());
    } catch (FileNotFoundException e ) {
      logger.warn("File not found, resetting score.");
      return 0;
    }
}

Quando colocamos referências que sãoAutoClosable em a declaraçãotry , então não precisamos fechar o recurso nós mesmos.

Ainda podemos usar um blocofinally, no entanto, para fazer qualquer outro tipo de limpeza que quisermos.

Confira nosso artigo dedicado atry-with-resources para saber mais.

4.5. Vários blocos decatch

Às vezes, o código pode lançar mais de uma exceção, e podemos ter mais de um blococatch tratar cada um individualmente:

public int getPlayerScore(String playerFile) {
    try (Scanner contents = new Scanner(new File(playerFile))) {
        return Integer.parseInt(contents.nextLine());
    } catch (IOException e) {
        logger.warn("Player file wouldn't load!", e);
        return 0;
    } catch (NumberFormatException e) {
        logger.warn("Player file was corrupted!", e);
        return 0;
    }
}

Várias capturas nos dão a chance de lidar com cada exceção de maneira diferente, se necessário.

Observe também que não capturamosFileNotFoundException, e isso é porqueextends IOException. Como estamos capturandoIOException, Java considerará qualquer uma de suas subclasses também tratada.

Digamos, porém, que precisamos tratarFileNotFoundException diferentemente doIOException mais geral:

public int getPlayerScore(String playerFile) {
    try (Scanner contents = new Scanner(new File(playerFile)) ) {
        return Integer.parseInt(contents.nextLine());
    } catch (FileNotFoundException e) {
        logger.warn("Player file not found!", e);
        return 0;
    } catch (IOException e) {
        logger.warn("Player file wouldn't load!", e);
        return 0;
    } catch (NumberFormatException e) {
        logger.warn("Player file was corrupted!", e);
        return 0;
    }
}

Java nos permite lidar com exceções de subclasse separadamente,remember to place them higher in the list of catches.

4.6. Uniãocatch Blocos

Quando sabemos que a maneira como lidamos com os erros será a mesma, o Java 7 introduziu a capacidade de capturar várias exceções no mesmo bloco:

public int getPlayerScore(String playerFile) {
    try (Scanner contents = new Scanner(new File(playerFile))) {
        return Integer.parseInt(contents.nextLine());
    } catch (IOException | NumberFormatException e) {
        logger.warn("Failed to load score!", e);
        return 0;
    }
}

5. Lançando exceções

Se não quisermos lidar com a exceção por conta própria ou se quisermos gerar nossas exceções para que outros possam lidar, então precisamos nos familiarizar com a palavra-chavethrow.

Digamos que tenhamos a seguinte exceção verificada que criamos:

public class TimeoutException extends Exception {
    public TimeoutException(String message) {
        super(message);
    }
}

e temos um método que pode levar muito tempo para ser concluído:

public List loadAllPlayers(String playersFile) {
    // ... potentially long operation
}

5.1. Lançar uma exceção verificada

Como retornar de um método, podemosthrow entar em qualquer ponto.

Obviamente, devemos jogar quando estamos tentando indicar que algo deu errado:

public List loadAllPlayers(String playersFile) throws TimeoutException {
    while ( !tooLong ) {
        // ... potentially long operation
    }
    throw new TimeoutException("This operation took too long");
}

ComoTimeoutException está marcado, também devemos usar a palavra-chavethrows na assinatura para que os chamadores de nosso método saibam como tratá-la.

5.2. Throwcantar uma exceção não verificada

Se queremos fazer algo como, por exemplo, validar entrada, podemos usar uma exceção não verificada:

public List loadAllPlayers(String playersFile) throws TimeoutException {
    if(!isFilenameValid(playersFile)) {
        throw new IllegalArgumentException("Filename isn't valid!");
    }

    // ...
}

ComoIllegalArgumentException está desmarcado, não precisamos marcar o método, embora sejamos bem-vindos.

Alguns marcam o método de qualquer maneira como uma forma de documentação.

5.3. Acondicionamento e relançamento

Também podemos optar por relançar uma exceção que detectamos:

public List loadAllPlayers(String playersFile)
  throws IOException {
    try {
        // ...
    } catch (IOException io) {
        throw io;
    }
}

Ou faça um wrap e relance novamente:

public List loadAllPlayers(String playersFile)
  throws PlayerLoadException {
    try {
        // ...
    } catch (IOException io) {
        throw new PlayerLoadException(io);
    }
}

Isso pode ser bom para consolidar muitas exceções diferentes em uma.

5.4. RetrocedendoThrowable ouException

Agora, para um caso especial.

Se as únicas exceções possíveis que um determinado bloco de código pode gerar são exceçõesunchecked, então podemos capturar e relançarThrowable ouException em adicioná-los à assinatura do nosso método:

public List loadAllPlayers(String playersFile) {
    try {
        throw new NullPointerException();
    } catch (Throwable t) {
        throw t;
    }
}

Embora simples, o código acima não pode lançar uma exceção verificada e por causa disso, embora estejamos relançando uma exceção verificada, não temos que marcar a assinatura com umathrows clause.

This is handy with proxy classes and methods. Mais sobre isso pode ser encontradohere.

5.5. Inheritance

Quando marcamos métodos com uma palavra-chavethrows, isso afeta como as subclasses podem sobrescrever nosso método.

Na circunstância em que nosso método lança uma exceção verificada:

public class Exceptions {
    public List loadAllPlayers(String playersFile)
      throws TimeoutException {
        // ...
    }
}

Uma subclasse pode ter uma assinatura "menos arriscada":

public class FewerExceptions extends Exceptions {
    @Override
    public List loadAllPlayers(String playersFile) {
        // overridden
    }
}

Mas não uma assinatura “more riskier”:

public class MoreExceptions extends Exceptions {
    @Override
    public List loadAllPlayers(String playersFile) throws MyCheckedException {
        // overridden
    }
}

Isso ocorre porque os contratos são determinados em tempo de compilação pelo tipo de referência. Se eu criar uma instância deMoreExceptions areia, salve-a emExceptions:

Exceptions exceptions = new MoreExceptions();
exceptions.loadAllPlayers("file");

Em seguida, a JVM só me dirácatchTimeoutException, o que está errado, pois eu disse queMoreExceptions#loadAllPlayers lança uma exceção diferente.

Simplificando, as subclasses podem lançarfewer exceções verificadas do que sua superclasse, mas nãomore.

6. Antipadrões

6.1. Exceções de engolir

Agora, há outra maneira de termos satisfeito o compilador:

public int getPlayerScore(String playerFile) {
    try {
        // ...
    } catch (Exception e) {} // <== catch and swallow
    return 0;
}

The above is calledswallowing an exception. Na maioria das vezes, seria um pouco mesquinho para nós fazer isso porque não resolve o problemaand, evita que outro código seja capaz de resolver o problema também.

Há momentos em que há uma exceção verificada que temos certeza de que nunca acontecerá. In those cases, we should still at least add a comment stating that we intentionally ate the exception:

public int getPlayerScore(String playerFile) {
    try {
        // ...
    } catch (IOException e) {
        // this will never happen
    }
}

Outra maneira de "engolir" uma exceção é imprimir a exceção no fluxo de erro simplesmente:

public int getPlayerScore(String playerFile) {
    try {
        // ...
    } catch (Exception e) {
        e.printStackTrace();
    }
    return 0;
}

Melhoramos nossa situação um pouco, escrevendo o erro em algum lugar para um diagnóstico posterior.

Seria melhor, porém, usarmos um logger:

public int getPlayerScore(String playerFile) {
    try {
        // ...
    } catch (IOException e) {
        logger.error("Couldn't load the score", e);
        return 0;
    }
}

Embora seja muito conveniente para nós lidar com exceções dessa forma, precisamos ter certeza de que não estamos engolindo informações importantes que os chamadores de nosso código podem usar para resolver o problema.

Finalmente, podemos engolir inadvertidamente uma exceção, não incluindo-a como causa, quando lançamos uma nova exceção:

public int getPlayerScore(String playerFile) {
    try {
        // ...
    } catch (IOException e) {
        throw new PlayerScoreException();
    }
}

Aqui, nós nos elogiamos por alertar nosso chamador de um erro, maswe fail to include the IOException as the cause. Por causa disso, perdemos informações importantes que chamadores ou operadores poderiam usar para diagnosticar o problema.

Estaríamos melhor se fizéssemos:

public int getPlayerScore(String playerFile) {
    try {
        // ...
    } catch (IOException e) {
        throw new PlayerScoreException(e);
    }
}

Observe a diferença sutil de incluirIOException comocause dePlayerScoreException.

6.2. Usandoreturn em um blocofinally

Outra maneira de engolir exceções éreturn do blocofinally. Isso é ruim porque, retornando abruptamente, a JVM eliminará a exceção, mesmo que tenha sido lançada pelo nosso código:

public int getPlayerScore(String playerFile) {
    int score = 0;
    try {
        throw new IOException();
    } finally {
        return score; // <== the IOException is dropped
    }
}

De acordo com oJava Language Specification:

Se a execução do bloco try for concluída abruptamente por qualquer outro motivoR, então o bloco finally será executado e haverá uma escolha.

Se o blocofinally for concluído normalmente, a instrução try será concluída abruptamente pelo motivo R.

Se o blocofinally for concluído abruptamente pelo motivo S, a instrução try será concluída abruptamente pelo motivo S (e o motivo R será descartado).

6.3. Usandothrow em um blocofinally

Semelhante a usarreturn em um blocofinally, a exceção lançada em um blocofinally terá precedência sobre a exceção que surge no bloco catch.

Isso “apagará” a exceção original do blocotry, e perderemos todas as informações valiosas:

public int getPlayerScore(String playerFile) {
    try {
        // ...
    } catch ( IOException io ) {
        throw new IllegalStateException(io); // <== eaten by the finally
    } finally {
        throw new OtherException();
    }
}

6.4. Usandothrow comogoto

Algumas pessoas também caíram na tentação de usarthrow como uma declaraçãogoto:

public void doSomething() {
    try {
        // bunch of code
        throw new MyException();
        // second bunch of code
    } catch (MyException e) {
        // third bunch of code
    }
}

Isso é estranho, porque o código está tentando usar exceções para controle de fluxo, em oposição ao tratamento de erros.

7. Exceções e erros comuns

Aqui estão algumas exceções e erros comuns que todos encontramos de tempos em tempos:

7.1. Exceções verificadas

  • IOException - Esta exceção é normalmente uma maneira de dizer que algo na rede, sistema de arquivos ou banco de dados falhou.

7.2. RuntimeExceptions

  • ArrayIndexOutOfBoundsException - esta exceção significa que tentamos acessar um índice de array inexistente, como ao tentar obter o índice 5 de um array de comprimento 3.

  • ClassCastException – esta exceção significa que tentamos realizar uma conversão ilegal, como tentar converter aString emList. Normalmente podemos evitá-lo realizando schecksinstanceof defensivos antes de lançar.

  • IllegalArgumentException - esta exceção é uma forma genérica de dizermos que um dos métodos ou parâmetros do construtor fornecidos é inválido.

  • IllegalStateException - Esta exceção é uma forma genérica de dizermos que nosso estado interno, como o estado de nosso objeto, é inválido.

  • NullPointerException - Esta exceção significa que tentamos fazer referência a um objetonull. Normalmente podemos evitá-lo executando verificações defensivas denull ou usandoOptional.

  • NumberFormatException - Esta exceção significa que tentamos converter umString em um número, mas a string continha caracteres ilegais, como tentar converter “5f3” em um número.

7.3. Erros

  • StackOverflowError – esta exceção significa que o rastreamento de pilha é muito grande. Às vezes, isso pode acontecer em aplicativos massivos; no entanto, geralmente significa que temos alguma recursão infinita acontecendo em nosso código.

  • NoClassDefFoundError - esta exceção significa que uma classe falhou ao carregar devido a não estar no caminho de classe ou devido a uma falha na inicialização estática.

  • OutOfMemoryError - esta exceção significa que a JVM não tem mais memória disponível para alocar para mais objetos. Às vezes, isso ocorre devido a um vazamento de memória.

8. Conclusão

Neste artigo, examinamos os fundamentos do tratamento de exceções, bem como alguns exemplos de boas e más práticas.

Como sempre, todos os códigos encontrados neste artigo podem ser encontradosover on GitHub!