Exceções do Java são úteis: talvez você é que não saiba usá-las

Semana passada escrevi sobre como os programadores complicam o código Java. Volto este tema agora para falar um pouco sobre um recurso extremamente útil da linguagem que, acredito, é muito mal compreendido: as exceptions.

Uma visão histórica

Acredito que para dominar uma linguagem de programação um dos primeiros passos que devemos tomar é buscar entender o que motivou e quais princípios guiaram sua criação. Uma abordagem histórica cai muito bem neste momento, sendo assim recomendo que você leia o capítulo sobre exceções tal como redigido na especificação do Java 1.0, acessível neste link (observem a ironia).

A leitura desta especificação é muito interessante, especialmente quando vemos de forma explícita os principais objetivos dos designers na criação da linguagem naquele momento: prover portabilidade e robustez. Ao falarmos de exceções o que realmente nos interessa é o segundo princípio. Como esta robustez é obtida?

Antes um pouco de semântica: uma exception, como o próprio nome já nos diz, denota uma condição anormal que ocorre durante o fluxo de execução dos nossos programas.

 

A vida sem exceptions

Muitas linguagens de programação simplesmente finalizam a execução do software (pense em C ou Pascal) quando algo assim ocorre e não há alguma forma de tratamento do erro padronizada, outra alternativa é apenas retornar um código de erro que pode facilmente ser ignorado pelo programador (pense na função read do C retornando o valor -1, por exemplo).

Vamos a um exemplo rápido usando “pseudo C”. A linguagem possuí uma função chamada read (mencionada acima) que lê bytes em uma fonte de dados e a armazena em um buffer. Ela retorna o valor -1 caso algo de errado ocorra, 0 se chegamos ao final do arquivo e um valor positivo nos informando quantos bytes foram lidos. Veja o código abaixo:

char buffer[128];
read(arquivo, buffer, 128);
printf("Serei impresso?");
// operações importantes seriam executadas na sequência

Este é um código muito comum: o programador espera que o arquivo sempre exista, sendo assim, possuí a “certeza” de que a saída “Serei impresso?” sempre irá ser exposta em seu terminal. Mas nem sempre é assim: e se o arquivo imaginário sumir? Nosso ingênuo programador irá enfrentar problemas pois a execução do seu programa terminaria em algum ponto após esta impressão.

Talvez nosso programador pudesse escrever o código acima de uma forma diferente tal como no exemplo a seguir:

char buffer[128];
int resultadoLeitura = read(arquivo, buffer, 128);
if (resultadoLeitura < 0) {
     // tento corrigir a situação aqui
}

É uma alternativa, o código se tornou mais robusto, mas sua leitura não torna claro o que de fato ocorreu para termos um erro. O arquivo foi apagado? Seria um problema de permissão? Ainda pior: o código que motivou a escrita do programa, o fluxo principal (em condições ideais de temperatura e pressão) agora se encontra mesclado ao código de tratamento de erros.

Java veio com uma solução mais interessante. Visto que nossas classes são uma interface, por que não alertar seus clientes a respeito do que pode dar errado e, ainda melhor: força-los a tratar estas situações (o problema está neste “força-los”)?

Pensando como Gosling, Joy e Steele

java_language_spec

Clareza na escrita

Por mais incrível que possa parecer a diversos críticos atuais da linguagem, naquela época um dos objetivos era ter código menos verboso. O ideal é que o programador pudesse ver o fluxo principal do seu programa de uma forma simples, e o tratamento dos erros isoladamente, tal como no exemplo a seguir:

String conteudoArquivo(File arquivo) {
          try {
                // meu fluxo principal entra aqui
          } catch (FileNotFoundException ex) {
               // o que eu faço se o arquivo não existir?
         } catch (EOFException ex) {
              // e se o arquivo chegar ao fim antes do imaginado?
         } catch (IOException ex) {
              // e se for algum outro erro de I/O que não previ e
             // não seja como os que mostrei antes?
        }
}

É interessante como agora você sabe o quê pode ter dado errado, e consegue diferenciar de forma clara como tratar cada uma daquelas situações. Ainda melhor: o que realmente importa, o fluxo principal, está claramente isolado.

Uma exception é na realidade um desvio de fluxo. Talvez você lide com erros do tipo FileNotFound e IOException da mesma forma. Neste caso, como a primeira exception é uma subclasse da segunda, basta colocar um único bloco catch para esta.

Exceptions como contrato

 

Mais do que isto, acredito que muitos programadores simplesmente não saibam interpretar o código que encontram: imagine uma declaração de método como a abaixo:

void processeArquivos(File[] arquivos) throws FileNotFoundException

O método me diz:

“Recebo uma lista de arquivos em uma matriz como parâmetro. Conseguirei executar meu trabalho quando todos os arquivos forem acessíveis a mim. Se me passar algum deles que não seja, repasso a você, que me chamou, a responsabilidade de lidar com este problema para mim.”

O método, é um contrato, e a exception, uma validação de que o mesmo será cumprido. Se não for o caso, o fluxo deverá ser alterado para que o seja ou a responsabilidade para se resolver o problema, repassada a outro objeto (talvez o cliente do cliente).

Mais do que isto: um contrato válido é aquele bem definido. Fica fácil perceber quando quem escreveu o código não tem muita ideia a respeito do que está fazendo. Observe a declaração de método abaixo:

void processeArquivos(File[] arquivos) throws Throwable

O que este método me diz?

“Recebo uma lista de arquivos para serem processados, alguma coisa pode dar errado, mas não sei o que.”

Temos um meio contrato aqui: apenas sabemos que devemos enviar arquivos para este método. Não sei se todos devem realmente estar acessíveis, apenas os envio.

Exceptions checadas e não checadas. Pra quê?

Por que há as tais “checked exceptions” e “unchecked exceptions”? O que diferencia uma de outra? Uma interpretação rápida seria:

“Checked exception é aquela que é uma subclasse de java.lang.Exception e que, se eu disparar no corpo do meu método, tenho de incluir uma clausula throws. A outra não, eu apenas a disparo lá dentro e não aviso ninguém a respeito pois é uma subclasse de RuntimeException ou Error.”

O que não responde quase nada além de expor uma hierarquia de classes incompleta. A resposta é mais simples: há erros que são tratáveis e outros nem tanto. Erros tratáveis são aqueles que definem um contrato e os clientes conseguem ao menos tentar resolve-los quando ocorrem.

Por exemplo: um arquivo inacessível é um erro tratável. Se topei com um erro do tipo FileNotFound, talvez seja possível criar um novo arquivo para em seguida chamar aquela função ou procedimento novamente.

Por outro lado, se houver um crash do meu sistema operacional ou meu sistema de arquivos desaparecer, não há muito o que eu possa fazer. É um erro de tempo de execução (runtime). E a quantidade de problemas deste tipo que podem ocorrer é praticamente infinita: seu HD pode pegar fogo, ou seu HD pode ser removido, ou seu sistema operacional pode desaparecer, ou sua rede pode se tornar inacessível, ou alguém pode desligar o servidor, ou….

Por que as exceções do tipo Runtime não são “checked”? Vou pedir para Gosling, Joy e Steele uma força. Veja o que é dito na seção 11.2.2 da especificação:

“A informação disponível para o compilador Java, e o nível de análise que este executa, raramente são  suficientes para se descobrir que erros de tempo de execução poderão ocorrer, mesmo sendo óbvio para o programador. Obrigar o programador a declarar todas estas exceções seria apenas uma tarefa irritante para o desenvolvedor.” (tradução minha)

É interessante também ver o que os autores dizem na especificação ao nos dizerem por que a outra categoria de erros (java.lang.Error) não são checados (11.2.1):

“São problemas que podem ocorrer em inúmeros pontos de um programa e cuja solução é difícil ou impossível. Um programa escrito em Java que precisasse lidar com todos estes erros seria uma zona e sem sentido algum” (tradução minha)

Sendo assim, ao invés de obrigar o desenvolvedor a tratar cada um destes problemas, por que não força-lo a lidar apenas com o que pode ser tratado? Este é um dos principais motivadores: te forçar a escrever menos código e, quem sabe, escrever código de melhor qualidade.

Isto não quer dizer que você deva escrever código como o a seguir:

try {
    // aqui está meu fluxo principal
} catch (Throwable t) {
    // aqui lidarei com todos os problemas possíveis e impossíveis
    // dos multiversos
}

Quando escrevemos algo como “catch (Throwable)” estamos com uma das seguintes ideias na cabeça:

  • Vou ignorar qualquer tipo de erro que venha a ocorrer.
    (me faz lembrar do “on error resume next” do VB)
  • Todos os erros são iguais, sendo assim os tratarei todos da mesma forma.

Se um dos princípios norteadores da criação do Java foi a robustez, e estamos usando Java (ignore sua linguagem favorita por um momento), escrever código deste tipo é corromper a linguagem e se induzir ao erro.

Mais do que isto: checked exceptions permitem ao compilador verificar se você está lidando com as situações anômalas definidas no contrato das suas interfaces.

Como uso bem as exceções?

O principal motivador para a escrita deste post são as críticas que ouço a respeito do modo como a linguagem Java lida com exceções. É interessante como muitas pessoas se esquecem que o recurso foi incluído na linguagem para facilitar a vida do programador, e não complicá-la.

Curiosamente, a esmagadora maioria das críticas que vejo são motivadas pelo mal uso ou compreensão do recurso. Sendo assim, seguem algumas dicas:

  • Pense na declaração de exceções como a definição de um contrato bem definido: elas definem premissas, ou seja, aquilo que não deve ocorrer para que o código possa ser executado com sucesso.
  • Tire proveito da precisão: um “catch (Throwable)” não te possibilita lidar com as diferentes situações ou quebras de contrato que podem ocorrer durante a execução do sistema, você estará apenas criando um bloco catch que, no futuro, pode se tornar um verdadeiro monstrinho.
  • Entenda a diferença entre checked e unchecked exceptions.
  • Uma declaração de método que contém uma instrução throws seguida de 293847 tipos de exceção e algo como um “throws Throwable” são a mesma coisa.
  • Se checked exceptions são um problema para você, considere linguagens como Groovy que torna o tratar de exceções uma tarefa opcional

Espero com este texto ter clarificado alguns pontos a respeito de um dos aspectos mais interessantes da linguagem Java.

 

7 comentários em “Exceções do Java são úteis: talvez você é que não saiba usá-las”

  1. Em suma, não retorne null para indicar um erro que poderia estar numa exceção.

    Gostei do post. Espero que tenha um post sobre exceções em Groovy, comparando com o jeito usado em Java.

    1. Kico (Henrique Lobo Weissmann)

      Oi Mariane, obrigado.

      Na realidade, é bem mais do que isto: é usar as exceções para te informar o que deu errado e fortalecer o contrato.

      Já estou engatilhado aqui um próximo post justamente expondo este aspecto do recurso. :)

      1. verdade. simplifiquei demais : P

        Aproveitando o post e se inspirando no CMMI e no Richardson maturity model, vou criar aqui o meu maturity model sobre uso de exceções em Java:

        level 0: usa null pra indicar erros.
        level 1: usa uma exceção genérica
        level 2: usa as exceções mais especifícas possíveis
        level 3: usa as exceções mais especificas possíveis e saber usar Optional.

  2. Éderson Cássio

    E se eu contar que na minha infância, aprendendo VB, eu já usei “on error resume next” só para o programa não parar e eu poder fazer em seguida:
    se erro = isso, faça aquilo
    se erro = aquele outro, faça este outro
    segue com o fluxo “normal”…

  3. Pingback: Projete boas APIs com Java exceptions

Deixe uma resposta

Esse site utiliza o Akismet para reduzir spam. Aprenda como seus dados de comentários são processados.

Rolar para cima