Acho que você provavelmente já passou por uma situação em que executa um código e acaba com algo como NullPointerException , ClassCastException ou pior... Isso é seguido por um longo processo de depuração, análise, pesquisa no Google e assim por diante. As exceções são maravilhosas: elas indicam a natureza do problema e onde ele ocorreu. Se quiser refrescar a memória e aprender um pouco mais, dê uma olhada neste artigo: Exceções: marcado, desmarcado e personalizado .

Dito isso, pode haver situações em que você precise criar sua própria exceção. Por exemplo, suponha que seu código precise solicitar informações de um serviço remoto que está indisponível por algum motivo. Ou suponha que alguém preencha um pedido de cartão bancário e forneça um número de telefone que, por acidente ou não, já está associado a outro usuário no sistema.

Obviamente, o comportamento correto aqui ainda depende dos requisitos do cliente e da arquitetura do sistema, mas vamos supor que você tenha sido encarregado de verificar se o número de telefone já está em uso e lançar uma exceção, se estiver.

Vamos criar uma exceção:


public class PhoneNumberAlreadyExistsException extends Exception {

   public PhoneNumberAlreadyExistsException(String message) {
       super(message);
   }
}
    

Em seguida, vamos usá-lo quando realizarmos nossa verificação:


public class PhoneNumberRegisterService {
   List<String> registeredPhoneNumbers = Arrays.asList("+1-111-111-11-11", "+1-111-111-11-12", "+1-111-111-11-13", "+1-111-111-11-14");

   public void validatePhone(String phoneNumber) throws PhoneNumberAlreadyExistsException {
       if (registeredPhoneNumbers.contains(phoneNumber)) {
           throw new PhoneNumberAlreadyExistsException("The specified phone number is already in use by another customer!");
       }
   }
}
    

Para simplificar nosso exemplo, usaremos vários números de telefone codificados para representar um banco de dados. E, finalmente, vamos tentar usar nossa exceção:


public class CreditCardIssue {
   public static void main(String[] args) {
       PhoneNumberRegisterService service = new PhoneNumberRegisterService();
       try {
           service.validatePhone("+1-111-111-11-14");
       } catch (PhoneNumberAlreadyExistsException e) {
           // Here we can write to logs or display the call stack
		e.printStackTrace();
       }
   }
}
    

E agora é hora de pressionar Shift+F10 (se você estiver usando o IDEA), ou seja, executar o projeto. Isso é o que você verá no console:

exceção.CreditCardIssue
exceção.PhoneNumberAlreadyExistsException: O número de telefone especificado já está em uso por outro cliente!
na exceção.PhoneNumberRegisterService.validatePhone(PhoneNumberRegisterService.java:11)

Olhe para você! Você criou sua própria exceção e até a testou um pouco. Parabéns por esta conquista! Eu recomendo experimentar um pouco o código para entender melhor como ele funciona.

Adicione outra verificação — por exemplo, verifique se o número de telefone inclui letras. Como você provavelmente sabe, as letras são frequentemente usadas nos Estados Unidos para tornar os números de telefone mais fáceis de lembrar, por exemplo, 1-800-MY-APPLE. Sua verificação pode garantir que o número de telefone contenha apenas números.

Ok, então criamos uma exceção verificada. Tudo estaria bem e bom, mas...

A comunidade de programação é dividida em dois campos — aqueles que são a favor das exceções verificadas e aqueles que se opõem a elas. Ambos os lados apresentam argumentos fortes. Ambos incluem desenvolvedores de primeira linha: Bruce Eckel critica as exceções verificadas, enquanto James Gosling as defende. Parece que este assunto nunca será resolvido permanentemente. Dito isso, vamos ver as principais desvantagens de usar exceções verificadas.

A principal desvantagem das exceções verificadas é que elas devem ser tratadas. E aqui temos duas opções: manipulá-lo no local usando um try-catch ou, se usarmos a mesma exceção em muitos lugares, usar throws para lançar as exceções e processá-las em classes de nível superior.

Além disso, podemos acabar com código "padrão", ou seja, código que ocupa muito espaço, mas não faz muito trabalho pesado.

Os problemas surgem em aplicativos razoavelmente grandes com muitas exceções sendo tratadas: a lista de lances em um método de nível superior pode crescer facilmente para incluir uma dúzia de exceções.

public OurCoolClass() lança FirstException, SecondException, ThirdException, ApplicationNameException...

Os desenvolvedores geralmente não gostam disso e, em vez disso, optam por um truque: eles fazem com que todas as exceções verificadas herdem um ancestral comum — ApplicationNameException . Agora eles também devem capturar essa exceção ( checked !) em um manipulador:


catch (FirstException e) {
    // TODO
}
catch (SecondException e) {
    // TODO
}
catch (ThirdException e) {
    // TODO
}
catch (ApplicationNameException e) {
    // TODO
}
    

Aqui enfrentamos outro problema — o que devemos fazer no último bloco catch ? Acima, já processamos todas as situações esperadas, portanto, neste ponto, ApplicationNameException significa nada mais para nós do que " Exceção : ocorreu algum erro incompreensível". É assim que lidamos com isso:


catch (ApplicationNameException e) {
    LOGGER.error("Unknown error", e.getMessage());
}
    

E no final, não sabemos o que aconteceu.

Mas não poderíamos lançar todas as exceções de uma vez, assim?


public void ourCoolMethod() throws Exception {
// Do some work
}
    

Sim, poderíamos. Mas o que "throws Exception" nos diz? Que algo está quebrado. Você terá que investigar tudo de cima a baixo e se familiarizar com o depurador por um longo tempo para entender o motivo.

Você também pode encontrar uma construção que às vezes é chamada de "engolir exceção":


try {
// Some code
} catch(Exception e) {
   throw new ApplicationNameException("Error");
}
    

Não há muito o que acrescentar aqui a título de explicação — o código deixa tudo claro, ou melhor, deixa tudo pouco claro.

Claro, você pode dizer que não verá isso em código real. Bem, vamos examinar as entranhas (o código) da classe URL do pacote java.net . Siga-me se quiser saber!

Aqui está uma das construções na classe URL :


public URL(String spec) throws MalformedURLException {
   this(null, spec);
}
    

Como você pode ver, temos uma exceção verificada interessante — MalformedURLException . Aqui é quando pode ser lançado (e cito):
"se nenhum protocolo for especificado, ou um protocolo desconhecido for encontrado, ou a especificação for nula, ou a URL analisada não cumprir a sintaxe específica do protocolo associado."

Aquilo é:

  1. Se nenhum protocolo for especificado.
  2. Um protocolo desconhecido foi encontrado.
  3. A especificação é nula .
  4. A URL não obedece à sintaxe específica do protocolo associado.

Vamos criar um método que cria um objeto URL :


public URL createURL() {
   URL url = new URL("https://codegym.cc");
   return url;
}
    

Assim que você escrever essas linhas no IDE (estou codificando no IDEA, mas isso funciona até no Eclipse e no NetBeans), você verá isso:

Isso significa que precisamos lançar uma exceção ou agrupar o código em um bloco try-catch . Por enquanto, sugiro escolher a segunda opção para visualizar o que está acontecendo:


public static URL createURL() {
   URL url = null;
   try {
       url = new URL("https://codegym.cc");
   } catch(MalformedURLException e) {
  e.printStackTrace();
   }
   return url;
}
    

Como você pode ver, o código já é bastante detalhado. E nós aludimos a isso acima. Esta é uma das razões mais óbvias para usar exceções não verificadas.

Podemos criar uma exceção não verificada estendendo RuntimeException em Java.

As exceções não verificadas são herdadas da classe Error ou da classe RuntimeException . Muitos programadores acham que essas exceções não podem ser tratadas em nossos programas porque representam erros que não podemos esperar recuperar enquanto o programa está em execução.

Quando ocorre uma exceção não verificada, geralmente é causada pelo uso incorreto do código, passando um argumento nulo ou inválido.

Bem, vamos escrever o código:


public class OurCoolUncheckedException extends RuntimeException {
   public OurCoolUncheckedException(String message) {
       super(message);
   }

   public OurCoolUncheckedException(Throwable cause) {
       super(cause);
   }
  
   public OurCoolUncheckedException(String message, Throwable throwable) {
       super(message, throwable);
   }
}
    

Observe que criamos vários construtores para diferentes propósitos. Isso nos permite dar mais recursos à nossa exceção. Por exemplo, podemos fazer com que uma exceção nos forneça um código de erro. Para começar, vamos fazer um enum para representar nossos códigos de erro:


public enum ErrorCodes {
   FIRST_ERROR(1),
   SECOND_ERROR(2),
   THIRD_ERROR(3);

   private int code;

   ErrorCodes(int code) {
       this.code = code;
   }

   public int getCode() {
       return code;
   }
}
    

Agora vamos adicionar outro construtor à nossa classe de exceção:


public OurCoolUncheckedException(String message, Throwable cause, ErrorCodes errorCode) {
   super(message, cause);
   this.errorCode = errorCode.getCode();
}
    

E não vamos esquecer de adicionar um campo (quase esquecemos):


private Integer errorCode;
    

E, claro, um método para obter este código:


public Integer getErrorCode() {
   return errorCode;
}
    

Vamos olhar para toda a classe para que possamos verificar e comparar:

public class OurCoolUncheckedException extends RuntimeException {
   private Integer errorCode;

   public OurCoolUncheckedException(String message) {
       super(message);
   }

   public OurCoolUncheckedException(Throwable cause) {
       super(cause);
   }

   public OurCoolUncheckedException(String message, Throwable throwable) {

       super(message, throwable);
   }

   public OurCoolUncheckedException(String message, Throwable cause, ErrorCodes errorCode) {
       super(message, cause);
       this.errorCode = errorCode.getCode();
   }
   public Integer getErrorCode() {
       return errorCode;
   }
}
    

Ta-da! Nossa exceção acabou! Como você pode ver, não há nada particularmente complicado aqui. Vamos conferir em ação:


   public static void main(String[] args) {
       getException();
   }
   public static void getException() {
       throw new OurCoolUncheckedException("Our cool exception!");
   }
    

Quando executarmos nosso pequeno aplicativo, veremos algo como o seguinte no console:

Agora vamos aproveitar a funcionalidade extra que adicionamos. Acrescentaremos um pouco ao código anterior:


public static void main(String[] args) throws Exception {

   OurCoolUncheckedException exception = getException(3);
   System.out.println("getException().getErrorCode() = " + exception.getErrorCode());
   throw exception;

}

public static OurCoolUncheckedException getException(int errorCode) {
   return switch (errorCode) {
   case 1:
       return new OurCoolUncheckedException("Our cool exception! An error occurred: " + ErrorCodes.FIRST_ERROR.getCode(), new Throwable(), ErrorCodes.FIRST_ERROR);
   case 2:
       return new OurCoolUncheckedException("Our cool exception! An error occurred: " + ErrorCodes.SECOND_ERROR.getCode(), new Throwable(), ErrorCodes.SECOND_ERROR);
   default: // Since this is the default action, here we catch the third and any other codes that we have not yet added. You can learn more by reading Java switch statement
       return new OurCoolUncheckedException("Our cool exception! An error occurred: " + ErrorCodes.THIRD_ERROR.getCode(), new Throwable(), ErrorCodes.THIRD_ERROR);
}

}
    

Você pode trabalhar com exceções da mesma forma que trabalha com objetos. Claro, tenho certeza que você já sabe que tudo em Java é um objeto.

E veja o que fizemos. Primeiro, mudamos o método, que agora não lança, mas simplesmente cria uma exceção, dependendo do parâmetro de entrada. Em seguida, usando uma instrução switch-case , geramos uma exceção com o código e a mensagem de erro desejados. E no método principal, obtemos a exceção criada, obtemos o código de erro e o lançamos.

Vamos executar isso e ver o que obtemos no console:

Veja - imprimimos o código de erro que obtivemos da exceção e, em seguida, lançamos a própria exceção. Além do mais, podemos até rastrear exatamente onde a exceção foi lançada. Conforme necessário, você pode adicionar todas as informações relevantes à mensagem, criar códigos de erro adicionais e adicionar novos recursos às suas exceções.

Bem, o que você acha disso? Espero que tenha dado tudo certo para você!

Em geral, as exceções são um tópico bastante extenso e não muito claro. Haverá muito mais disputas sobre isso. Por exemplo, apenas Java verificou exceções. Entre as linguagens mais populares, não vi nenhuma que as utilizasse.

Bruce Eckel escreveu muito bem sobre exceções no capítulo 12 de seu livro "Thinking in Java" — recomendo que você leia! Também dê uma olhada no primeiro volume do "Core Java" de Horstmann - Ele também tem muitas coisas interessantes no capítulo 7.

um pequeno resumo

  1. Escreva tudo em um log! Mensagens de log em exceções lançadas. Isso geralmente ajudará muito na depuração e permitirá que você entenda o que aconteceu. Não deixe um bloco catch vazio, caso contrário, ele apenas "engolirá" a exceção e você não terá nenhuma informação para ajudá-lo a encontrar problemas.

  2. Quando se trata de exceções, é uma prática ruim pegar todas de uma vez (como disse um colega meu, "não é Pokemon, é Java"), então evite catch (Exception e) ou pior, catch ( Throwable t ) .

  3. Lance exceções o mais cedo possível. Esta é uma boa prática de programação Java. Ao estudar frameworks como o Spring, você verá que eles seguem o princípio de "falha rápida". Ou seja, eles "falham" o mais cedo possível para possibilitar a localização rápida do erro. Claro, isso traz certos inconvenientes. Mas essa abordagem ajuda a criar um código mais robusto.

  4. Ao chamar outras partes do código, é melhor capturar certas exceções. Se o código chamado lançar várias exceções, é uma prática de programação ruim capturar apenas a classe pai dessas exceções. Por exemplo, digamos que você chame o código que lança FileNotFoundException e IOException . Em seu código que chama esse módulo, é melhor escrever dois blocos catch para capturar cada uma das exceções, em vez de um único catch para capturar Exception .

  5. Capture exceções somente quando puder manipulá-las efetivamente para usuários e para depuração.

  6. Não hesite em escrever suas próprias exceções. Claro, Java tem muitos já prontos, algo para cada ocasião, mas às vezes você ainda precisa inventar sua própria "roda". Mas você deve entender claramente por que está fazendo isso e certificar-se de que o conjunto padrão de exceções ainda não tenha o que você precisa.

  7. Ao criar suas próprias classes de exceção, tenha cuidado com a nomenclatura! Você provavelmente já sabe que é extremamente importante nomear classes, variáveis, métodos e pacotes corretamente. Exceções não são exceção! :) Sempre termine com a palavra Exception , e o nome da exceção deve transmitir claramente o tipo de erro que ela representa. Por exemplo, FileNotFoundException .

  8. Documente suas exceções. Recomendamos escrever uma tag Javadoc @throws para exceções. Isso será especialmente útil quando seu código fornecer interfaces de qualquer tipo. E você também achará mais fácil entender seu próprio código posteriormente. O que você acha, como você pode determinar do que se trata MalformedURLException ? De Javadoc! Sim, a ideia de escrever documentação não é muito atraente, mas acredite, você vai agradecer a si mesmo quando retornar ao seu próprio código seis meses depois.

  9. Libere recursos e não negligencie a construção de tentativa com recursos .

  10. Aqui está o resumo geral: use as exceções com sabedoria. Lançar uma exceção é uma operação razoavelmente "cara" em termos de recursos. Em muitos casos, pode ser mais fácil evitar o lançamento de exceções e, em vez disso, retornar, digamos, uma variável booleana que indica se a operação foi bem-sucedida, usando um if- else simples e "mais barato" .

    Também pode ser tentador vincular a lógica do aplicativo a exceções, o que você claramente não deve fazer. Como dissemos no início do artigo, as exceções são para situações excepcionais, não esperadas, e existem diversas ferramentas para preveni-las. Em particular, há Optional para evitar um NullPointerException , ou Scanner.hasNext e similares para evitar um IOException , que o método read() pode lançar.