Olá, estudante! Em nossa última lição, relembramos os conceitos de herança, polimorfismo e interface e, nessa altura da disciplina, é evidente que coisas podem dar errado durante uma programação, por isso dedicamos uma lição toda para falar sobre exceções e erros na programação Java. Abordaremos os mecanismos de tratamento de exceções, a hierarquia de classes de exceções e como utilizar blocos try-catch para lidar com diferentes tipos de situações inesperadas.
Além disso, exploraremos boas práticas no tratamento de exceções para melhorar a robustez e a manutenção do código. Todo esse conhecimento será essencial para que, enquanto desenvolvedor(a), você busque criar aplicações Java mais resilientes e eficientes. Ao final desta Lição, você terá uma compreensão sólida de como lidar com situações excepcionais e erros durante a execução de um programa Java.
Imagine que você está desenvolvendo um sistema de software para um banco, onde os clientes podem realizar operações financeiras online, como transferências, pagamentos e consultas de saldo. Durante o processo de implementação, surge um questionamento: como lidar com situações inesperadas e erros possíveis de ocorrer durante as transações financeiras? Por exemplo, o que acontecerá se um cliente tentar realizar uma transferência para uma conta inexistente e inserir um valor inválido? Ou se houver uma falha de conexão com o banco de dados no meio de uma transação?
A utilização de exceções em Java pode ser a resposta para esse desafio! Ao implementar um sistema de tratamento de exceções adequado, é possível antecipar e gerenciar cenários inesperados, mantendo a integridade das transações e proporcionando uma experiência mais segura aos usuários. Com essa utilização, haverá benefícios de manutenção do fluxo de execução, mensagens de erros claras e proteção contra situações inesperadas.
Portanto, nesse cenário, o uso efetivo de exceções em Java revela-se crucial para assegurar a estabilidade e confiabilidade do sistema bancário online, protegendo tanto os dados dos clientes quanto a reputação da instituição financeira. Assim, a implementação cuidadosa de tratamento de exceções contribui significativamente com a construção de um software mais resiliente, capaz de oferecer uma experiência consistente aos usuários.
O case hipotético da lição de hoje é sobre uma empresa de desenvolvimento de software contratada por um hospital para criar um sistema de gestão hospitalar que pudesse otimizar o gerenciamento de pacientes, agendamentos e registros médicos. Durante a implementação, a equipe de desenvolvimento enfrentou um desafio significativo relacionado ao tratamento de erros.
Durante os testes iniciais, os desenvolvedores perceberam que o sistema não estava lidando adequadamente com situações inesperadas, como consultas marcadas para horários já ocupados, tentativas de acesso a registros médicos inexistentes ou falhas na comunicação com o banco de dados. No intuito de resolver esses problemas, a equipe optou por utilizar exceções e tratamentos de erros em Java. Cada módulo do sistema foi revisado, por meio da identificação de pontos críticos onde poderiam ocorrer falhas, em seguida, foram implementadas exceções personalizadas para capturar esses eventos indesejados.
O sistema de gestão hospitalar, agora equipado com um robusto sistema de tratamento de exceções, foi capaz de lidar eficazmente com diversas situações imprevistas, o que, por sua vez, resultou em um software mais estável, confiável e capaz de atender às demandas críticas de um ambiente hospitalar. O sucesso desse projeto ressalta a importância do uso estratégico de exceções e tratamentos de erros na construção de sistemas tão confiáveis quanto resilientes.
Vamos aprender como você, futuro(a) desenvolvedor(a) de sistemas, poderá realizar as exceções em Java?
Antes de iniciar nosso aprofundamento em erros e exceções, é importante compreender a diferença entre essas duas palavras. Quando falamos em erro, este indica problemas graves e, geralmente, irreversíveis, que ocorrem durante a execução do programa. Como exemplo de erros, podemos citar falta de memória e falhas no sistema, ou seja, falamos de situações que, normalmente, não podem ser recuperadas e não são tratadas pelo programa, pois estão fora do controle do desenvolvedor. Já uma exceção representa situações excepcionais e recuperáveis durante a execução do programa. Como exemplo de exceção, temos: divisão por zero, acesso a um índice inválido em um array, falha na abertura de um arquivo. A exceção deve ser tratada pelo programa para se tornar mais robusta e controlada. De acordo com Deitel e Deitel, (2016) uma exceção é uma indicação de um problema que ocorre durante a execução de um programa e cujo tratamento permite criar aplicativos capazes de resolvê-lo, ou ao menos, tratá-lo.
Compreendendo essa diferenciação, você, futuro(a) Técnico(a) em Desenvolvimento de Sistemas, pode adotar estratégias específicas para lidar com exceções e tomar ações apropriadas. Porém, erros, geralmente, são irreversíveis e, frequentemente, resultam na interrupção do programa.
Especificamente na linguagem de programação Java, temos uma hierarquia das classes que refletem as exceções. Quando falamos de hierarquia de exceções em Java, precisamos compreender que isso diz respeito a uma estrutura organizada de classes, a qual reflete diferentes tipos de problemas possíveis de ocorrer durante a execução de um programa. Segundo Deitel e Deitel (2016), essas exceções podem ser localizadas por toda a Java API, como você verá, a seguir:
A classe raiz da hierarquia de exceções em Java. Todas as exceções, tanto aquelas que podem ser tratadas quanto aquelas que não podem, são subclasses diretas ou indiretas de throwable. São suas subclasses diretas: error e exception.
Subclasse de throwable que representa problemas graves e, geralmente, irreversíveis não tratados pelo programa. De modo geral, o error indica falhas no sistema ou condições que estão além do controle da pessoa desenvolvedora. São exemplos de error: OutOfMemoryError e StackOverflowError.
Subclasse de throwable que representa exceções capazes de ocorrer durante a execução normal do programa. As exceções dessa classe ou suas subclasses podem ser tratadas pelo programa, para evitar a interrupção da execução. São subclasses diretas de exception: RuntimeException (exceções de tempo de execução) e CheckedException (exceções verificadas).
Dentro da hierarquia de exception, as RuntimeExceptions são frequentemente associadas a erros de lógica no código e não precisam ser explicitamente tratadas (ex.: NullPointerException). Já as CheckedExceptions devem ser tratadas ou declaradas no método (ex.: IOException). Essa distinção ajuda os desenvolvedores a lidar de forma adequada com diferentes tipos de exceções.
São estruturas de código que permitem aos desenvolvedores monitorar e tratar exceções durante a execução do programa. De acordo com Deitel e Deitel (2016), um bloco try consiste na palavra-chave “try” seguida por um bloco de código entre chaves {}. O código dentro do bloco try é monitorado quanto à ocorrência de exceções durante a execução, conforme você poderá ver na Figura 1:
Já o bloco catch é executado quando ocorre uma exceção específica dentro do bloco try. Deitel e Deitel (2016) afirmam que o bloco catch inicia com a palavra-chave “catch” seguido por um parâmetro entre parênteses () e um bloco de código entre chaves {}. Cada bloco catch está associado a um tipo específico de exceção e, se a exceção especificada ocorrer, esse bloco correspondente será executado, como você pode conferir, novamente, na Figura 1.
Dentro do bloco catch, você pode incluir código para lidar com a exceção, como fornecer uma mensagem de erro, registrar informações ou até mesmo corrigir a situação, dependendo do contexto. A utilização de vários blocos catch permite tratar, de maneira específica, diferentes tipos de exceções.
Como visto no tópico anterior, em um bloco try, você pode ter vários blocos catch, cada um associado a um tipo específico de exceção. Deitel e Deitel (2016) indicam ser relativamente comum que, para tratar vários tipos de exceção, um bloco try seja seguido por vários blocos catch. Dessa forma, o código dentro de cada bloco catch será executado somente se a exceção correspondente ocorrer durante a execução do bloco try, conforme você poderá ver na Figura 2:
O uso de múltiplos blocos catch oferece a flexibilidade de tratar, de maneira específica, diferentes tipos de exceções. Cada bloco catch lida com um tipo particular de exceção, proporcionando um controle mais granular sobre o tratamento de erros no programa, o que permite aos desenvolvedores implementar lógicas específicas de recuperação ou de registro de erros conforme necessário, melhorando a capacidade de diagnóstico e de resposta a falhas durante a execução do software.
É uma parte opcional associada a um bloco try-catch. O código dentro do bloco finally será executado independentemente de ocorrer ou não uma exceção no bloco try.
O bloco finally é comumente utilizado para ações que precisam ser realizadas, como liberação de recursos, independentemente de ocorrer uma exceção. Observe a Figura 3:
O bloco finally oferece uma maneira de garantir que determinadas ações críticas sejam realizadas, mesmo se uma exceção for lançada, contribuindo para a robustez e integridade do programa. Assim, são asseguradas a liberação adequada de recursos e a manutenção consistente do estado do sistema, independentemente de erros ocorrerem ou não durante a execução.
Em Java, é possível criar classes de exceção personalizadas, estendendo a classe exception ou uma de suas subclasses. Essas exceções personalizadas podem ser usadas para representar condições específicas do programa que requerem tratamento especial e, ao serem criadas, é possível que essas exceções forneçam informações mais contextuais sobre o erro, facilitando a identificação e resolução de problemas. Observe um exemplo na Figura 4:
A criação de exceções personalizadas contribui para um design mais claro e semântico, melhorando a manutenção e compreensão do código, quando se fornece uma maneira estruturada e específica de lidar com diferentes cenários de erro. Essa ação promove uma gestão de exceções mais eficiente, facilita tanto a identificação rápida quanto o tratamento adequado de problemas específicos durante o desenvolvimento e a operação de software.
Na Figura 4, você pôde conferir o uso da palavra-chave throw. Pois bem, a declaração de exceções usando throws e o lançamento explícito de exceções usando throw são recursos importantes em Java, para lidar com situações específicas. Vamos compreender melhor esses recursos?
No cabeçalho de um método, é possível usar a palavra-chave throws para declarar que um método pode lançar determinadas exceções. Isso informa aos chamadores do método as exceções que precisam ser tratadas. Deitel e Deitel (2016) apontam que essas exceções são capazes de serem lançadas por instruções no corpo do método ou por métodos chamados a partir dele.
Dentro do corpo de um método, essa palavra-chave é usada para lançar explicitamente uma exceção. Tal recurso é útil quando uma condição específica no método não pode ser tratada localmente e precisa ser comunicada aos chamadores.
O uso de throws ajuda a documentar as exceções possíveis de serem lançadas por um método, enquanto throw permite sinalizar problemas específicos durante a execução do método. Ambos são essenciais para a manipulação eficaz de exceções em Java, garantindo que os desenvolvedores entendam rapidamente quais exceções podem surgir em um método e permitindo o tratamento apropriado dessas situações, a fim de manter a estabilidade bem como o comportamento controlado do programa em diferentes cenários de erro.
No desenvolvimento de software em Java, o tratamento de exceções é uma prática essencial para garantir a robustez e a confiabilidade das aplicações. Elas representam situações inesperadas que podem ocorrer durante a execução de um programa, como erros de lógica, operações inválidas ou problemas de acesso a recursos.
A seguir, serão compartilhadas algumas das exceções mais frequentemente encontradas em aplicações Java, cada uma com uma breve descrição de sua função e o contexto no qual pode ser lançada:
NullPointerException: lançada quando se tenta acessar um método ou campo de um objeto que é null.
ArrayIndexOutOfBoundsException: lançada quando ocorre uma tentativa de acesso a um índice inválido em um array.
ArithmeticException: lançada quando ocorre uma operação aritmética indefinida, como divisão por zero.
FileNotFoundException: lançada quando um código tenta abrir um arquivo que não existe no sistema de arquivos.
IOException: exceção de entrada/saída geral, frequentemente lançada por operações de leitura ou gravação em arquivos ou streams.
NumberFormatException: lançada quando uma operação de conversão de string para número não pode ser realizada devido a um formato inválido.
IllegalArgumentException: lançada quando um argumento transmitido para um método não atende aos requisitos especificados.
IllegalStateException: lançada quando o estado de um objeto não é consistente com a operação solicitada.
ClassCastException: lançada quando ocorre uma tentativa de fazer um cast para um tipo de objeto incompatível.
NoSuchElementException: lançada por métodos como nextElement() de coleções, quando não há mais elementos disponíveis.
StackOverflowError: lançada quando a pilha de chamadas de métodos atinge a sua capacidade máxima.
Compreender e lidar adequadamente com essas exceções em Java é fundamental para o desenvolvimento de aplicações mais seguras e confiáveis, pois, ao antecipar os cenários onde essas exceções podem ocorrer, você será capaz de implementar soluções preventivas e corretivas que minimizem os riscos de falhas durante a execução do programa.
Outra vantagem que você terá: o tratamento eficaz das exceções contribui para a melhoria da experiência do usuário, o que evita interrupções inesperadas e garante que o software opere conforme o esperado, mesmo diante de situações adversas.
O conteúdo da lição de hoje é de extrema importância para você, futuro(a) Técnico(a) em Desenvolvimento de Sistemas, consolidar sua compreensão de como desenvolver aplicações Java mais robustas e confiáveis. Ao dominar o tratamento de exceções, você estará preparado(a) para lidar com situações inesperadas durante a execução dos programas, garantindo que o código seja mais resiliente e menos propenso a falhas.
Agora, chegou a hora de aplicar os conceitos de exceções e erros em Java, por meio da seguinte implementação:
Abra seu navegador web e acesse o site OnlineGDB, em https://www.onlinegdb.com.
Escolha a linguagem de programação que deseja usar. Selecione “Java” na lista suspensa.
No editor de código, você pode escrever o código Java com tratamento de exceções, conforme a Figura 5.
Clique no botão “Run” para compilar e executar o código.
Observe a saída na seção de “Output”. O programa tentará acessar um índice inválido em um array, e os blocos catch tratarão as exceções específicas. O bloco finally será executado independentemente de ocorrer uma exceção ou não.
Esse exemplo demonstra como usar múltiplos blocos catch para tratar diferentes tipos de exceções em Java. Lembre-se de que a ordem desses blocos é importante, pois o primeiro bloco que corresponder ao tipo de exceção será executado.
Agora, discuta com seus colegas e professores como criar novas situações nas quais o uso das exceções e seus respectivos tratamentos possam ser utilizados.
DEITEL, P.; DEITEL H. Java: Como programar. 10. ed. São Paulo: Pearson, 2016.