Saltar a contenido

Excepciones

Introducción

En el proceso de ejecución de un programa, pueden surgir errores que como programadores pueden ser difíciles de controlar. Por ejemplo

  • cuando tenemos que leer un fichero y este no existe
  • cuando el usuario tiene que introducir un número e introduce un texto
  • cuando hacemos una petición a una base de datos y no contesta...

Por ejemplo, las siguientes muestras de código provocan la parada del programa

int[] numeros={1,2,3,4};
//provocará un error porque nos salimos tamaño del array
System.out.println(numeros[6]);// java.lang.ArrayIndexOutOfBoundsException

String s="Hola";
//provocará un error al convertir el string a entero
int numero=Integer.parseInt(s);//java.base/java.lang.NumberFormatException

//provocará error al dividir por cero
int resultado=12/0;//java.lang.ArithmeticException: / by zero

int[] lista;
//provocará error nullPointerException, el objeto no existe
int size= lista.length

Los programas tienes que ser robustos y recuperarse ante errores. Para manejar los errores y que los programas no se queden “colgados” y sigan su funcionamiento tenemos el manejo de excepciones.

Si el error no se trata, el manejador de excepciones realiza lo siguiente:

  • Muestra la descripción de la excepción.
  • Muestra la traza de la pila de llamadas.
  • Provoca el final del programa

Exception

Excepción

Las excepciones son un mecanismo especial para gestionar errores. Permiten separar el tratamiento de errores del código normal de un programa.

Una excepción es un evento que ocurre durante la ejecución de una aplicación e interrumpe el flujo normal de las instrucciones del programa.

Exception

Class Exception y class Error extienden Throwable.

La clase Error está relacionada con errores de la máquina virtual de Java. Generalmente estos errores no dependen del programador, por lo que no nos debemos preocupar por tratarlos, por ejemplo, OutOfMemoryError, StackOverflowError, errores de hardware, etc.

En la clase Exception se encuentran las excepciones que se pueden lanzar en una aplicación

Las excepciones son diferentes de los errores porque se pueden escribir programas para recuperarse de excepciones, pero no se pueden escribir programas para recuperarse de errores.

Las excepciones pueden ser detectadas por una parte del programa que intenta recuperarse del problema.

Qué ocurre cuando se produce una excepción

  • La Máquina Virtual Java crea un objeto excepción y lo lanza. El objeto excepción creado contiene información sobre el error. La ejecución normal del programa se detiene.
  • El sistema busca en el método donde se ha producido la excepción un manejador de excepciones que capture(catch) ese objeto y trate la excepción.
  • Si el método no contiene un manejador para la excepción se busca en el método que llamó a este y así sucesivamente en toda la pila de llamadas.

  • Cuando se encuentra un manejador apropiado se le pasa la excepción. Un manejador de excepciones es considerado apropiado si el tipo de objeto excepción lanzado es compatible al tipo que puede manejar.

  • Si no se encuentra un manejador adecuado la Máquina Virtual Java muestra el error y acaba el programa. Por ejemplo, si tenemos un programa que tiene una serie de llamadas a métodos para hacer una división y finalmente el usuario introduce un 0 en el denominador
public class ExcepPiladellamadas {

    static void incluirNumerador(){
        Scanner sc = new Scanner(System.in);
        System.out.print("Indica el numerador: ");
        int num = sc.nextInt();
        incluirDenominador(num);
    }
    static void incluirDenominador(int n){
        Scanner sc = new Scanner(System.in);
        System.out.print("Indica el denominador: ");
        int den = sc.nextInt();//si se introduce un 0
        realizarDivisión(n, den);
    }

    static void realizarDivisión(int n, int d){
        System.out.println("La división es: " + n/d);
    }

    public static void main(String[] args) {
        incluirNumerador();
    }

}

Exception

Jerarquía de Excepciones

En la clase Exception se encuentran las excepciones que se pueden lanzar en una aplicación. Tiene varias subclases, entre ellas:

  • RuntimeException: son excepciones lanzadas durante la ejecución del programa. Por ejemplo: ArithmeticException, NullPointerException, ArrayIndexOutOfBoundsException, etc. y pertenecen al paquete java.lang.
  • IOException: son excepciones lanzadas al ejecutar una operación de entrada-salida. Pertenecen al paquete java.io.
  • ClassNotFoundException: excepción lanzada cuando una aplicación intenta cargar una clase, pero no se encuentra el fichero .class correspondiente.

Podemos ver un resumen

Exception

Tratamiento de excepciones

Un programa que trate una excepción, tiene que un bloque del tipo

try {
    //Instrucciones que se intentan ejecutar, si se produce una
    //situación inesperada se lanza una excepción
} 
catch(tipoExcepcion e){
    //Instrucciones para tratar esta excepción
}
catch(otroTipoExcepcion e){ 
    //Instrucciones para tratar esta excepción
}
    //Se pueden escribir tantos bloques catch como sean necesarios
finally{//opcional
    //instrucciones que se ejecutarán siempre después de un bloque try,
    //se haya producido o no una excepción
}

Bloque try: dentro del bloque try se coloca el código que podría generar una excepción.

Bloques catch: capturan y tratan una excepción cuando esta ocurre. Pueden existir varios bloques catch. Estos se definen directamente después del bloque try. Ningún código puede estar entre el final del bloque try y el comienzo del primer bloque catch. Los catch se evalúan por orden, si un catch atrapa la excepción que ha ocurrido, se ejecuta y los demás no.

Bloque finallly: es opcional e incluye código que se ejecuta siempre, independientemente si se ha producido una excepción o no

Ejemplos

El siguiente programa lee un número entero y lo muestra. Si en la instrucción sc.nextInt() se introduce un número de otro tipo o un carácter, se lanza una excepción InputMismatchException que es capturada por el bloque catch. En este bloque se realizan las instrucciones necesarias para resolver la situación y que el programa pueda continuar.

public static void main(String[] args) {
    Scanner sc = new Scanner(System.in);
    int n;
    do{
        try{
            System.out.print("Introduce un número entero > 0: ");
            n = sc.nextInt();
            System.out.println("Número introducido: " + n);
        }
        catch(InputMismatchException e){
            sc.nextLine();//retorna la tecla enter
            n = 0;
            System.out.println("Debe introducir un número entero "
            + e.toString());
        }
    }while(n<=0);
}
El siguiente ejemplo captura dos excepciones, por un lado se espera la entrada de dos enteros y por otro que el segundo no sea cero

try{
    //produce error si no se introduce un entero
    System.out.print("Introduce dividendo: ");
    n1 = sc.nextInt();
    System.out.println();
    System.out.print("Introduce divisor: ");
    n2 = sc.nextInt();
    //produce un error si n2 es 0
    System.out.println(n1/n2);            
        }
catch (InputMismatchException e){
    System.out.println("Error: hay que introducir un entero");
}
catch (ArithmeticException e){
    System.out.println("Error: no se puede dividir por 0");
}

Importante

Cuando se lanza una excepción se captura por el primer bloque catch cuyo parámetro sea de la misma clase que el objeto excepción o de una clase base directa o indirecta. Por este motivo, es importante el orden en que se coloquen los bloques catch.

Las excepciones más genéricas se deben capturar al final. Si no es necesario tratar excepciones concretas de forma específica se puede poner un bloque catch de una clase base que las capture todas y las trate de forma general. Esto se conoce como captura genérica de excepciones.

En el siguiente ejemplo, en el último catch, se captura cualquier posible error que surja

public static void main(String[] args) {
    Scanner sc = new Scanner(System.in);
    int [] array = {4,2,6,7};
    int n;
    boolean repetir = false;
    do{
        try{
            repetir = false;
            System.out.print("Introduce un número entero > 0 y < " +
                    array.length +
                    " ");
            n = sc.nextInt();
            System.out.println("Valor en la posición " + n + ": " +
                    array[n]);
        }
        catch(InputMismatchException e){
            sc.nextLine();
            n = 0;
            System.out.println("Debe introducir un número entero ");
            repetir = true;
        }
        catch(IndexOutOfBoundsException e){
            System.out.println("Debe introducir un número entero > 0 y <" + array.length + " ");
            repetir = true;
        }
        // resto de excepciones de tipo Exception y derivadas
        catch(Exception e){ 
            System.out.println("Error inesperado " + e.toString());
            repetir = true;
        }
    }while(repetir);
}

Tipo de exepciones

En Java, hay dos tipos de excepciones:

  • Checked exceptions (Excepciones verificadas).
  • Unchecked exceptions (Excepciones no verificadas).

Exception

Checked exceptions

Estas son las excepciones que se comprueban en tiempo de compilación, es decir, un método debe hacer algo al respecto sino el programa no compila. El programador está obligado a tratarla.

No son culpa del programador de Java. Por ejemplo, un programa que debe acceder a una imagen en una carpeta. Si esa imagen no se encuentra en la ubicación esperada porque alguien la ha movido, borrado o cambiado el nombre, dará error, no siendo responsabilidad del programador.

Si algún código dentro de un método arroja una excepción verificada, entonces el método puede una de las dos posibilidades:

  1. Manejar la excepción en un bloque try-catch{}
public void leerFichero(String[] a) {
   try {
           //código java manejo de ficheros
    } catch (IOException e) {
        // Capturamos la excepción en caso de que ocurra y manejamos el error
        System.err.println("Error de entrada/salida (IOException): " + e.getMessage());
        e.printStackTrace();
    }
}
  1. throws (lanzar) la excepción al que invoca el método.
public void leerFichero(String[] a) throws IOException {
    //código java manejo de ficheros
}

En el ejemplo, IOException es una checked exception. Este método lanza una IOException cuando hay un problema de lectura.

La palabra reservada throws dice que este método no captura la excepción IOException con un catch, sino que cuando ocurra una, se lanzará al método que llama el método leerFichero.

Si hiciéramos esto en un método main, podemos capturar la excepción lanzada por el método.

public static void main(String[] args) throws IOException {
    try {
          leerFichero();
    } catch (IOException e) {
        // Capturamos la excepción en caso de que ocurra y manejamos el error
        System.err.println("Error de entrada/salida (IOException): " + e.getMessage());
        e.printStackTrace();
    }

}

Unchecked exceptions

Ocurren en tiempo de compilación, es decir, no se verifican por el compilador. En Java, las excepciones en las clases RunTimeException y Error y sus subclases son unchecked exceptions todo lo demás es es checked.

Depende de los programadores especificar o detectar las excepciones unchecked. Por ejemplo, imagina que tenemos el siguiente programa Java se compila sin erorres:

public static void main(String[] args) {
    int x = 0;
    int y = 10;
    int z = y / x;
}

El compilador nos deja compilar la aplicación porque ArithmeticException es una excepción no verificada, pero al ejecutar el programa se lanza la excepción, por tanto, nosotros tendremos que darnos cuenta y controlar este tipo de excepciones.

Diferencias entre checked y unchecked exceptions

  • Una checked exception se detecta en tiempo de compilación, mientras que una unchecked exception en tiempo de ejecución.
  • Una checked exception debe manejarse o bien con volviéndola a lanzar con un throw o con un bloque try catch, mientras que una unchecked exception no requiere ser manejada.
  • Una unchecked exception es un error de programación y es fatal, mientras que una checked exception es una condición de excepción dentro de la lógica de su código y se puede recuperar o volver a intentar.

Exception

Throwable

Throwable es la superclase de todas las excepciones y errores. Se podria capturar en un bloque try-catch, ¡pero nunca se debe hacer!, ya que, no solo capturará todas las excepciones; sino que también hará lo mismo con todos los errores que genere la aplicacion.

La JVM arroja errores para indicar problemas graves que no están destinados a ser manejados por una aplicación.

Ejemplos típicos de eso son OutOfMemoryError o StackOverflowError. Ambos son causados por situaciones que están fuera del control de la aplicación y no se pueden manejar.

Por lo tanto, es mejor que NUNCA captures un Throwable.

Relanzar una excepción

Java permite al programador lanzar excepciones mediante la palabra reservada throw:

throw objetoExcepcion;

La excepción que se lanza es un objeto, por lo que hay que crearlo como cualquier otro objeto mediante new.

if (n==0) throw new ArithmeticException(División por cero);

Argumentos protegidos mediante excepciones

Los métodos y los setter de una clase pueden necesitar ciertas características que se deben cumplir debido a las especificaciones del problema. Por ejemplo, al introducir una nota de un alumno tiene que estar en el rango de 0..10

Enviar un mensaje desde la propia clase al usuario puede que no sea procedente ya que la clase tiene que ocuparse de que los datos sean correctos pero no de interactuar con el usuario. Piensa en un entorno gráfico.

Podemos lanzar excepciones cuando se trate de introducir un valor ilegal y que se trate el error más arriba

public class Nota {
    String asignatura;
    int nota;

    public Nota(String asignatura) {
        this.asignatura = asignatura;
    }

    public int getNota() {
        return nota;
    }

    public void setNota(int nota) {
        //si la nota no cumple las especificaciones, lanzamos una excepción
        if(nota<0 || nota >10)
            throw new IllegalArgumentException("La nota tiene que estar en el rango 0..10");
        this.nota = nota;
    }

    public static void main(String[] args) {
        Nota programacion=new Nota("Programación");
        programacion.setNota(11);
    }
}
En este ejemplo, el programa termina porque no se ha capturado la excepción

Exception

Lo correcto es controlar la excepción. De esta forma, podemos leer del usuario la nota y controlar mediante excepciones el valor correcto

public static void main(String[] args) {
    Scanner sc = new Scanner(System.in);
    Nota programacion = new Nota("Programación");
    boolean esCorrecto = true;
    int valorNota;
    do {//mientras la nota no sea correcta salta la excepción
        esCorrecto = true;
        try {
            System.out.println("Introduzca una nota entre 0..10");
            valorNota = sc.nextInt();
            //si no es correcta, salta la excepción
            programacion.setNota(valorNota);

        } catch (IllegalArgumentException e) {
            esCorrecto = false;
        }
    } while (!esCorrecto);

}

Excepciones personalizadas

Aunque las excepciones de Java cubren casi todas las excepciones generales que están obligadas a ocurrir en la programación. Sin embargo, a veces necesitamos complementar estas excepciones estándar con las nuestras.

Estas son las principales razones para introducir excepciones personalizadas:

  • Añadir excepciones que son específicas de la lógica de nuestra aplicación o empresa. Éstas ayudan a los usuarios de la aplicación o a los desarrolladores a comprender cuál es el problema exacto.
  • Para capturar y proporcionar un tratamiento específico a un subconjunto de excepciones de Java existentes.
  • Añadir nuevos métodos o atributos que no son parte de las excepciones estándar.

Buenas prácticas para definir excepciones personalizadas

Haz que tenga sentido

Proporciona información o funcionalidad que no forma parte de las excepciones estándar de Java.

Esa es la esencia de la primera y más importante recomendación. De lo contrario, su excepción no brinda ningún beneficio en comparación con la gran cantidad de excepciones que ya proporciona el JDK.

Si no puede proporcionar ningún beneficio, es mejor que use una de las excepciones estándar, como UnsupportedOperationException o IllegalArgumentException. Todos los desarrolladores de Java ya conocen estas excepciones. Eso hace que su código y API sean más fáciles de entender.

Sigue la convención de nomenclatura estándar

Cuando obsevamos las clases de excepción proporcionadas por el JDK, rápidamente nos damos cuenta que todos sus nombres terminan con Exception. Esta convención de nomenclatura general se utiliza en todo el sistema de Java. Y su excepción personalizada también debería seguirlo, por ejemplo, ImpresoraException.

Añade Javadoc a tu clase de excepción personalizada

Las API no documentadas son muy difíciles de usar.

Las clases de excepción pueden no ser la parte más obvia de tu API, pero siguen siendo parte de ella. Tan pronto como uno de sus métodos orientados al cliente arroja una excepción, la clase de excepción se convierte en parte de la API. Eso implica que requiere documentación y un buen Javadoc.

El Javadoc debe describir el significado general de la excepción y las situaciones en las que podría ocurrir. El objetivo es ayudar a otros desarrolladores a comprender su API y evitar escenarios de error comunes.

Proporciona un constructor

Normalmente, nuestro código detectará una excepción estándar antes de lanzar nuestra excepción personalizada. Esto, no debe ocultarse ya que la excepción capturada generalmente contendrá información esencial que necesitaremos para analizar el incidente.

En el siguiente ejemplo, NumberFormatException proporciona información detallada sobre el error. Perderá esta información si no la configura como la causa de MyException.

public void methodA(String entrada) throws MyException {
    try {
        // código
    } catch (NumberFormatException e) {
        throw new MyException("Mensaje que describe el error.", e, ErrorCode.INVALID_ENTRY);
    }
}

Exception y RuntimeException proporcionan constructores que aceptan un Throwable que describe la causa de la excepción. Por tanto, nuestra excepción debería hacer lo mismo. Debemos implementar al menos un constructor que obtenga el Throwable causante como parámetro y lo establezca en la superclase.

Pasos para implementar una excepción personalizada

  1. Debemos extender de la clase java.lang.Exception.
  2. Se debe proporcionar un constructor que establezca la excepción causante y brinde un beneficio en comparación con las excepciones estándar disponibles.

Ejemplo:

Exception

Exception

MyException usa un enum personalizado para almacenar un código de error que identifica el problema de nuestra aplicación. Los clientes pueden usar el código de error para mostrar mensajes de error localizados o decirle al usuario que incluya este código en un ticket de soporte.

Para usar MyException en nuestra aplicación o programa necesitaremos especificarlo como parte de la signatura de un método o simplemente capturarla con un bloque try-catch.

Exception

Note

Si queremos crear una excepción personalizada de tipo unchecked exception realizaremos el mismo procedimiento con la diferencia de que tendremos que extender de la clase java.lang.RuntimeException.