Probablemente hayas experimentado una situación en la que ejecutas un código y te encuentras con algo como una NullPointerException, ClassCastException, o peor... Esto es seguido por un largo proceso de depuración, análisis, búsqueda en Google, y así sucesivamente. Las excepciones son maravillosas tal como son: indican la naturaleza del problema y dónde ocurrió. Si quieres refrescar tu memoria y aprender un poco más, echa un vistazo a este artículo: Excepciones: comprobadas, no comprobadas y personalizadas.

Dicho esto, puede haber situaciones en las que necesites crear tu propia excepción. Por ejemplo, supongamos que tu código necesita solicitar información de un servicio remoto que no está disponible por alguna razón. O supongamos que alguien completa una solicitud de tarjeta de crédito y proporciona un número de teléfono que, ya sea por accidente o no, ya está asociado con otro usuario en el sistema.

Por supuesto, el comportamiento correcto aquí sigue dependiendo de los requisitos del cliente y la arquitectura del sistema, pero vamos a suponer que se te ha encargado verificar si el número de teléfono ya está en uso y lanzar una excepción si es así.

Creemos una excepción:


public class PhoneNumberAlreadyExistsException extends Exception {

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

A continuación, lo usaremos cuando realicemos nuestra verificación:


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 nuestro ejemplo, usaremos varios números de teléfono codificados para representar una base de datos. Y finalmente, intentemos usar nuestra excepción:


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();
       }
   }
}
    

Y ahora es el momento de presionar Shift+F10 (si estás usando IDEA), es decir, ejecutar el proyecto. Esto es lo que verás en la consola:

exception.CreditCardIssue
exception.PhoneNumberAlreadyExistsException: ¡El número de teléfono especificado ya está en uso por otro cliente!
at exception.PhoneNumberRegisterService.validatePhone(PhoneNumberRegisterService.java:11)

Mira nada más lo que has logrado! Has creado tu propia excepción y hasta la has probado un poco. ¡Felicidades por este logro! Te recomiendo experimentar un poco con el código para entender mejor cómo funciona.

Agrega otra verificación, por ejemplo, verifica si el número de teléfono incluye letras. Como probablemente sepas, en los Estados Unidos a menudo se utilizan letras para hacer que los números de teléfono sean más fáciles de recordar, por ejemplo, 1-800-MY-APPLE. Tu verificación podría asegurarse de que el número de teléfono solo contenga números.

Vale, entonces hemos creado una excepción verificada. Todos estarían bien, pero...

La comunidad de programadores se divide en dos bandos: los que están a favor de las excepciones verificadas y los que se oponen a ellas. Ambos lados hacen argumentos fuertes. Ambos incluyen desarrolladores de primera categoría: Bruce Eckel critica las excepciones verificadas, mientras que James Gosling las defiende. Parece que este asunto nunca se resolverá definitivamente. Dicho esto, veamos las principales desventajas de usar excepciones verificadas.

La principal desventaja de las excepciones verificadas es que deben manejarse. Y aquí tenemos dos opciones: manejarlas en su lugar usando un try-catch, o, si usamos la misma excepción en muchos lugares, usar throws para lanzar las excepciones hacia arriba y procesarlas en las clases de nivel superior.

También podemos terminar con código "boilerplate", es decir, código que ocupa mucho espacio, pero no hace mucho trabajo pesado.

Los problemas surgen en aplicaciones bastante grandes con muchas excepciones que se manejan: la lista de throws en un método de nivel superior puede crecer fácilmente hasta incluir una docena de excepciones.

public NuestraClaseGenial() throws PrimeraExcepcion, SegundaExcepcion, TerceraExcepcion, NombreDeLaAplicacionException...

A los desarrolladores generalmente no les gusta esto y, en cambio, optan por un truco: hacen que todas sus excepciones verificadas hereden de un ancestro común: NombreDeLaAplicacionException. Ahora también deben capturar esa excepción (verificada!) en un controlador:


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

Aquí nos enfrentamos a otro problema: ¿qué deberíamos hacer en el último bloque catch? Arriba, ya procesamos todas las situaciones esperadas, por lo que en este punto, ApplicationNameException no significa más para nosotros que "Exception: ocurrió algún error incomprensible". Así es como lo manejamos:


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

Y al final, no sabemos lo que sucedió.

Pero ¿no podríamos lanzar todas las excepciones de una vez, así?


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

Sí, podríamos hacerlo. Pero, ¿qué nos dice "throws Exception"? Que algo está roto. Tendrás que investigar todo de arriba abajo y hacerte amigo del depurador durante mucho tiempo para entender la razón.

También puede encontrarse con una construcción que a veces se llama "tragar excepciones":


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

No hay mucho que agregar aquí en términos de explicación, el código lo hace todo claro, o mejor dicho, lo hace todo poco claro.

Por supuesto, se podría decir que no se verá esto en código real. Bueno, echemos un vistazo a las entrañas (el código) de la clase URL del paquete java.net. ¡Sígueme si quieres saberlo!

Aquí hay una de las construcciones en la clase URL:


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

Como se puede observar, tenemos una interesante excepción controlada llamada MalformedURLException. Aquí está cuando puede ser lanzada (y cito):
"si no se especifica ningún protocolo, se encuentra un protocolo desconocido, spec es null o la URL analizada no cumple con la sintaxis específica del protocolo asociado."

Es decir:

  1. Si no se especifica ningún protocolo.
  2. Se encuentra un protocolo desconocido.
  3. El spec es null.
  4. La URL no cumple con la sintaxis específica del protocolo asociado.

Crearemos un método que cree un objeto URL:


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

Tan pronto como escribas estas líneas en el IDE (Estoy usando IDEA, pero esto funciona incluso en Eclipse y NetBeans), verás esto:

Esto significa que necesitamos lanzar una excepción, o envolver el código en un bloque try-catch. Por ahora, sugiero elegir la segunda opción para visualizar lo que está sucediendo:


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

Como puedes ver, el código ya es bastante verboso. Y ya lo mencionamos anteriormente. Esta es una de las razones más obvias para usar excepciones no comprobadas.

Podemos crear una excepción no comprobada extendiendo RuntimeException en Java.

Las excepciones no comprobadas se heredan de la clase Error o de la clase RuntimeException en Java. Muchos programadores sienten que estas excepciones no se pueden manejar en nuestros programas porque representan errores de los que no podemos esperar recuperarnos mientras se esté ejecutando el programa.

Cuando ocurre una excepción no comprobada, generalmente se debe al uso incorrecto del código, al pasar un argumento nulo o inválido.

Bueno, escribamos el 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);
   }
}
    

Ten en cuenta que hemos creado varios constructores para diferentes propósitos. Esto nos permite darle más capacidades a nuestra excepción. Por ejemplo, podemos hacer que una excepción nos dé un código de error. Para empezar, creemos un enum para representar nuestros códigos de error:


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;
   }
}
    

Ahora agreguemos otro constructor a nuestra clase de excepción:


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

Y no olvidemos agregar un campo (casi lo olvidamos):


private Integer errorCode;
    

Y, por supuesto, un método para obtener este código:


public Integer getErrorCode() {
   return errorCode;
}
    

Veamos toda la clase para poder revisarla y compararla:

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;
   }
}
    

¡Listo! ¡Nuestra excepción está terminada! Como puedes ver, no hay nada particularmente complicado aquí. Vamos a verla en acción:


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

Cuando ejecutemos nuestra pequeña aplicación, veremos algo como lo siguiente en la consola:

Ahora aprovechemos la funcionalidad adicional que hemos agregado. Agreguemos un poco al 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);
}

}
    

Puedes trabajar con excepciones de la misma manera que trabajas con objetos. Por supuesto, estoy seguro de que ya sabes que todo en Java es un objeto.

Y mira lo que hicimos. Primero, cambiamos el método, que ahora no lanza, sino que simplemente crea una excepción, dependiendo del parámetro de entrada. A continuación, utilizando una instrucción switch-case, generamos una excepción con el código de error y el mensaje deseado. Y en el método principal, obtenemos la excepción creada, obtenemos el código de error y la lanzamos.

Ejecutemos esto y veamos qué obtenemos en la consola:

Mira, hemos impreso el código de error que obtuvimos de la excepción y luego lanzamos la excepción en sí. Además, incluso podemos rastrear exactamente dónde se produjo la excepción. Según sea necesario, puedes agregar toda la información relevante al mensaje, crear códigos de error adicionales y agregar nuevas funciones a tus excepciones.

Bueno, ¿qué opinas de eso? ¡Espero que todo haya funcionado para ti!

En general, las excepciones son un tema bastante extenso y no está del todo claro. Habrá muchas más discusiones al respecto. Por ejemplo, solo Java tiene excepciones comprobadas. Entre los lenguajes más populares, no he visto ninguno que las utilice.

Bruce Eckel escribió muy bien sobre excepciones en el capítulo 12 de su libro "Thinking in Java" — ¡te recomiendo que lo leas! También echa un vistazo al primer volumen de "Core Java" de Horstmann — también tiene muchas cosas interesantes en el capítulo 7.

Un pequeño resumen

  1. ¡Escribe todo en un registro! Registra los mensajes en las excepciones lanzadas. Esto generalmente ayudará mucho en la depuración y te permitirá comprender qué sucedió. No dejes un bloque catch vacío, de lo contrario simplemente "tragará" la excepción y no tendrás ninguna información para ayudarte a solucionar problemas.

  2. Cuando se trata de excepciones, es una mala práctica capturarlas todas de una vez (como dijo un colega mío, "no es Pokemon, es Java"), por lo que evita catch (Exception e) o peor aún, catch (Throwable t).

  3. Lanza excepciones lo antes posible. Esta es una buena práctica de programación en Java. Cuando estudies frameworks como Spring, verás que siguen el principio de "fallar rápidamente". Es decir, "fallan" lo antes posible para poder encontrar rápidamente el error. Por supuesto, esto trae ciertas molestias. Pero este enfoque ayuda a crear un código más robusto.

  4. Cuando llames a otras partes del código, es mejor capturar ciertas excepciones. Si el código llamado lanza varias excepciones, es una mala práctica de programación capturar solo la clase padre de esas excepciones. Por ejemplo, supongamos que llamas al código que lanza una FileNotFoundException y una IOException. En tu código que llama a este módulo, es mejor escribir dos bloques de captura para capturar cada una de las excepciones, en lugar de un solo catch para capturar Exception.

  5. Captura excepciones solo cuando puedas manejarlas de manera efectiva para los usuarios y para la depuración.

  6. No dudes en escribir tus propias excepciones. Por supuesto, Java tiene muchas listas para usar, algo para cada ocasión, pero a veces todavía necesitas inventar tu propio "rueda". Pero debes entender claramente por qué estás haciendo esto y estar seguro de que el conjunto estándar de excepciones no tiene lo que necesitas.

  7. Cuando crees tus propias clases de excepción, ¡ten cuidado con el nombre! Probablemente ya sepas que es extremadamente importante nombrar correctamente clases, variables, métodos y paquetes. ¡Las excepciones no son una excepción! :) Siempre termina con la palabra Exception, y el nombre de la excepción debe transmitir claramente el tipo de error que representa. Por ejemplo, FileNotFoundException.

  8. Documente sus excepciones. Recomendamos escribir una etiqueta Javadoc @throws para las excepciones. Esto será especialmente útil cuando su código proporcione interfaces de cualquier tipo. Y también encontrará más fácil entender su propio código más tarde. ¿Qué piensa usted, cómo puede determinar de qué se trata la MalformedURLException? ¡De Javadoc! Sí, la idea de escribir documentación no es muy atractiva, pero créame, se lo agradecerá cuando regrese a su propio código seis meses después.

  9. Libere los recursos y no descuide la construcción try-with-resources.

  10. Aquí está el resumen general: use las excepciones sabiamente. Lanzar una excepción es una operación bastante "costosa" en términos de recursos. En muchos casos, puede ser más fácil evitar lanzar excepciones y en su lugar devolver, por ejemplo, una variable booleana que indique si la operación tuvo éxito, utilizando un if-else simple y "menos costoso".

    También puede ser tentador vincular la lógica de la aplicación a excepciones, lo cual no debería hacer. Como dijimos al principio del artículo, las excepciones son para situaciones excepcionales, no esperadas, y hay varias herramientas para prevenirlas. En particular, existe Optional para evitar una NullPointerException, o Scanner.hasNext y similares para prevenir una IOException, que puede lanzar el método read().