Logo DIE

Excepciones y Errores

Unidad de Apoyo para el Aprendizaje

Iniciar

Introducción


La ley de Pareto, también conocida como la regla 80/20, establece que el 80 % de las consecuencias proviene del 20 % de las causas.

En el desarrollo de software se dice que el 80 % del esfuerzo (en tiempo y recursos) produce el 20 % del código. Asimismo, en términos de calidad, el 80 % de las fallas de una aplicación son producidas por el 20 % del código. Por lo tanto, detectar y manejar errores es la parte más importante en una aplicación robusta.

Una aplicación puede tener diversos tipos de errores, los cuales se pueden clasificar en tres grandes grupos:

  • Errores sintácticos: fallas generadas por infringir las normas de escritura de un lenguaje o sintaxis; por ejemplo, coma, punto y coma, dos puntos, palabras reservadas mal escritas, entre otras. Normalmente estos errores son detectados por el compilador o el intérprete (dependiendo del lenguaje de programación utilizado) al procesar el código fuente.

  • Errores semánticos o lógicos: defectos más sutiles, se producen cuando la sintaxis del código es correcta, pero la semántica o significado no es el que se pretendía. Un error semántico puede hacer que el programa termine de forma anormal o con resultados incoherentes.

  • Errores de ejecución: se presentan cuando la aplicación se está ejecutando e impiden que continúe con este proceso. Su origen es diverso, se pueden producir por un uso incorrecto del programa por parte del usuario (si ingresa una cadena cuando se espera un número), o se pueden presentar debido a errores de programación (acceder a localidades no reservadas o hacer divisiones entre cero), o debido a algún recurso externo al programa (al acceder a un archivo o al conectarse a algún servidor o cuando se acaba el espacio en la pila o stack de la memoria).





Identificar bloques de código propensos a generar errores y aplicar técnicas adecuadas para el manejo de situaciones excepcionales en tiempo de ejecución.

Excepciones


Una excepción es una condición anormal que cuando ocurre altera el flujo normal del programa en ejecución. Estos errores pueden ser generados por la lógica del programa, como un índice de un arreglo fuera de su rango, una división entre cero o errores generados por los propios objetos que denuncian algún tipo de estado no previsto o condición que no pueden manejar.

Desde el punto de vista del tratamiento de una excepción dentro de un programa, hay que tener en cuenta que todas estas clases de excepción se dividen en dos grandes grupos:

Diagrama de bloques


Jerarquía de clases de las excepciones y errore

Jerarquía de clases de las excepciones y errores



En Java, cuando un evento excepcional ocurre se dice que se lanza una excepción. Las excepciones son el mecanismo mediante el cual se pueden controlar los errores producidos en tiempo de ejecución. El código responsable de hacer alguna acción cuando se arroja una excepción es llamado manejador de excepciones y lo que hace es “cachar la excepción lanzada”.





Manejo de excepciones


Para manejar una excepción se utilizan las palabras reservadas try y catch. El bloque try es utilizado para definir el bloque de código en el cual una excepción puede ocurrir. El o los bloques catch son utilizados para definir un bloque de código que maneje la excepción.

Las excepciones son objetos que contienen información del error que se ha producido; heredan de la clase Exception que a su vez hereda de la clase Throwable.

Ejemplo

Aquí se intenta acceder a los elementos de un arreglo, en este caso el número de elementos del arreglo son tres (índices 0, 1 y 2) pero en el ciclo for se llega hasta el índice 3, lo que está fuera de los límites del arreglo y genera una excepción del tipo ArrayIndexOutOfBoundsException.

1 public class TryCatch {
2     public static void main(String[] args) {
3            try {
4                String mensajes[] = {"Primero", "Segundo", "Tercero" };
5                for (int i=0; i<=3; i++) {
6                      System.out.println(mensajes[i]);
7                }
8            } catch ( ArrayIndexOutOfBoundsException e ) {
9                System.out.println("\nError: índice fuera del arreglo.");
10           }
11           System.out.println("\nContinúa el flujo del programa.");
12       }
13 }

Si un código lanza una excepción y ésta no se captura, entonces interviene un manejador por defecto que normalmente imprime información que ayuda a encontrar dónde y cómo se produjo el error. Sin embargo, como la excepción no fue controlada, ésta se propaga hasta llegar al método principal y termina abruptamente el programa.

Ejemplo

1 public class TryCatch {
2       public static void main(String[] args) {
3           String mensajes[] = {"Primero", "Segundo", "Tercero" };
4           for (int i=0; i<=3; i++) {
5                System.out.println(mensajes[i]);
6           }
7           System.out.println("\nContinua el flujo del programa.");
8      }
9 }

La palabra reservada finally permite definir un tercer bloque de código dentro del manejador de excepciones. Este bloque le indica al programa las instrucciones a ejecutar de manera independiente de los bloques try-catch, es decir, si el código del bloque try se ejecuta de manera correcta, entra al bloque finally; si se genera un error, después de ejecutar el código del bloque catch ejecuta el código del bloque finally.

Ejemplo

El siguiente ejemplo trata de hacer una división entre cero, lo que genera una ArithmeticException; sin embargo, el bloque finally se va a ejecutar.

1 public class TryCatchFinally {
2      public static void main(String[] args) {
3            String mensajes[] = {"Primero", "Segundo", "Tercero" };
4            try {
5                 float equis = 5/0;
6                System.out.println("Equis = " + equis);
7            } catch ( ArithmeticException e ) {
8                System.out.println("\nError: división entre cero.");
9            } finally {
10                System.out.println("\nEl bloque finally siempre se ejecuta.");
11          }
12          System.out.println("\nContinua el flujo del programa.");
13       }
14 }

Aunque no se genere una excepción (la división se hace entre 2 en este caso) el bloque finally se ejecuta:

1 public class TryCatchFinally {
2     public static void main(String[] args) {
3           String mensajes[] = {"Primero", "Segundo", "Tercero" };
4           try {
5                float equis = 5/2;
6                System.out.println("Equis = " + equis);
7           } catch ( ArithmeticException e ) {
8                System.out.println("\nError: división entre cero.");
9           } finally {
10                System.out.println("\nEl bloque finally siempre se ejecuta.");
11           }
12           System.out.println("\nContinua el flujo del programa.");
13      }
14 }

Es posible tener más de un bloque catch dentro del manejador de excepciones, sólo se debe cuidar el orden de captura de las excepciones, es decir, las excepciones se deben acomodar de las más específicas a las más generales, como se muestra a continuación:

catch (ClassNotFoundException e) {...}
catch (IOException e) {...}
catch (Exception e) {...}

Si se invierte el orden, se capturan las excepciones de lo más general a lo más específico, todas las excepciones caerían en las primeras (Exception es la más general) y no habría manera de enviar un error del tipo IOExcepction o ClassNotFoundException. Esta situación la detecta el compilador y no permite generar el bytecode, es decir, no compila.

Ejemplo

Ejemplo

Propagación de excepciones


En un programa no es obligatorio tratar las excepciones que puede generar un bloque de código, pero sí se debe indicar explícitamente a través del método que éste puede arrojar una excepción. A su vez, el método superior deberá incluir los bloques try/catch o volver a pasar la excepción. De esta forma se puede ir propagando la excepción de un método a otro hasta llegar al método main.



Ejemplo

1 public class PropagaExcepcion {
2      public static int miMetodo(int a, int b){
3           int c = a / b;
4           return c;
5      }
6
7      public static void main(String [] args){
8           try {
9                int division = miMetodo(10, 0);
10                System.out.println(division);
11           } catch (Exception e){
12                System.out.println("Excepción aritmética arrojada: ");
13                e.printStackTrace();
14           }
15           System.out.println("El programa continúa.");
16      }
17 }

En el ejemplo anterior se está capturando el error que produce el método (división entre cero en este caso). Sin embargo, si la excepción no se captura, el error se propaga (se sigue arrojando) hasta que se capture o se llegue al método principal y se acabe abruptamente la ejecución. ¡Pruébalo! Quita el bloque try-catch del método main, sólo conserva las líneas 9 y 10 y observa qué ocurre.



Throws

Existen métodos que obligan a ejecutarse dentro de un manejador de excepciones (es decir, son excepciones marcadas), debido a que el método define que va a lanzar una excepción.

Para indicar que un método puede lanzar una excepción se utiliza la palabra reservada throws seguida de la o las excepciones que puede arrojar dicho método. La sintaxis es la siguiente:

[modificadores] valorRetorno nombreMetodo() throws Excepcion1, Excepcion2 {
// Bloque de código del método
}

Esta sintaxis obliga que el método que mande llamar a nombreMetodo() debe manejar la excepción o indicar que va a arrojar la misma excepción (propagar).

Ejemplo

La primera posibilidad sería llamar al método miMetodo() que arroja la excepción dentro de un bloque try-catch:

1 public class PropagaExcepcion {
2      public static int miMetodo(int a, int b) throws ArithmeticException {
3           int c = a / b;
4           return c;
5      }
6
7      public static void main(String [] args){
8           try {
9                int division = miMetodo(10, 0);
10              System.out.println(division);
11          } catch (Exception e){
12                System.out.println("Excepción aritmética arrojada: ");
13                e.printStackTrace();
14          }
15          System.out.println("El programa continua.");
16     }

La segunda opción es que el método que manda llamar miMetodo() indique que puede arrojar la misma excepción:

1 public class PropagaExcepcion {
2     public static int miMetodo(int a, int b) throws ArithmeticException {
3          int c = a / b;
4          return c;
5      }
6
7     public static void main(String [] args) throws ArithmeticException {
8          int division = miMetodo(10, 0);
9          System.out.println(division);
10          System.out.println("El programa continua.");
11    }
12 }

Para practicar

Ejecuta ambas opciones y observa qué sucede.



Throw

Una excepción se puede lanzar de manera explícita (sin necesidad de que ocurra un error) creando una instancia de ésta y arrojándola mediante la palabra reservada throw. Una excepción se debe arrojar dentro de un manejador de excepciones o de un método que indique que se va arrojar dicha excepción o cualquier subclase. La sintaxis es la siguiente:

throw new Excepcion();

Ejemplo

1 public class PropagaExcepcion {
2      public static int miMetodo(int a, int b) throws ArithmeticException {
3           if (b==0)
4                throw new ArithmeticException("División entre 0.");
5           int c = a / b;
6           return c;
7      }
8
9      public static void main(String [] args) {
10           try {
11                int division = miMetodo(10, 0);
12                System.out.println(division);
13           } catch (Exception e){
14                System.out.println("Excepción aritmética arrojada: ");
15                e.printStackTrace();
16           }
17           System.out.println("El programa continua.");
18      }
19 }

Excepciones propias

Cuando se desarrollan aplicaciones es común requerir excepciones que no están definidas en el API de Java, es decir, excepciones propias del negocio. Es posible crear clases que se comporten como excepciones (que se puedan arrojar), para ello sólo es necesario heredar de Exception o de Throwable. Estas excepciones se invocan y se pueden arrojar de la misma manera en la que se utilizan las excepciones del API; pero pueden generar información más concisa del problema debido a que se están programando a la medida del negocio.

Ejemplo

Para una aplicación bancaria se desea lanzar una excepción propia cuando el saldo de una cuenta sea insuficiente para realizar una operación (por ejemplo, un retiro).

Se crea la clase de excepción propia SaldoInsuficienteException:

1 public class SaldoInsuficienteException extends Exception {
2      public SaldoInsuficienteException(){
3           super("Saldo insuficiente");
4      }
5 }

Se crea la clase Cuenta, la cual podrá lanzar una excepción de tipo SaldoInsuficienteException si se intenta retirar un monto mayor al saldo de la cuenta:

1 public class Cuenta {
2      private double saldo;
3
4      public Cuenta(){
5           this.saldo = 0;
6      }
7
8      public void depositar(double monto){
9            System.out.println("Depositando " + monto);
10          this.saldo += monto;
11     }
12
13      public void retirar (double monto) throws SaldoInsuficienteException {
14           System.out.println("Retiando " + monto);
15           if (this.saldo < monto)
16                throw new SaldoInsuficienteException();
17           else
18                saldo -= monto;
19      }
20
21      public double getSaldo(){
22           return this.saldo;
23      }
24 }

Finalmente, para probar el funcionamiento de la clase Cuenta y su correspondiente excepción, se crea una cuenta Cajero donde se emulan depósitos y retiros a una cuenta:

1 public class Cajero {
2      public static void main (String [] args){
3           Cuenta cuenta = new Cuenta();
4
5           try{
6              cuenta.depositar(2000);
7               cuenta.retirar(1000);
8               cuenta.getSaldo();
9               cuenta.retirar(1000);
10              cuenta.getSaldo();
11              cuenta.retirar(1000);
12              cuenta.getSaldo();
13              cuenta.depositar(200);
14               cuenta.retirar(200);
15           } catch (SaldoInsuficienteException e){
16                System.out.println("Mensaje: " + e.getMessage());
17                e.printStackTrace();
18           }
19      }
20 }

Los métodos getMessage y printStackTrace se heredan de Exception. El método getMessage imprime la cadena que se envía al objeto cuando se construye. El método printStackTrace imprime la traza del error, es decir, el método donde se generó el problema y todos los métodos que se invocaron antes de éste.

ícono

Actividad. Estructura de control try-catch-finally

En los programas existen líneas de código que pueden generar errores al ser ejecutadas y, con ello, hacer que el programa termine abruptamente. Para evitar que esto suceda, Java cuenta con la estructura try-catch-finally.

ícono

Autoevaluación. Tipos de errores y maneras de controlarlos

Para generar software de calidad es importante contemplar todas las posibles vertientes que éste puede tomar.

Sin embargo, esto es prácticamente imposible, por lo que se debe contemplar que los programas pueden generar errores y que deben ser tratados para evitar que el programa termine abruptamente. De aquí la importancia de conocer los tipos de errores que existen en la generación de software y la manera en las que se pueden controlar.


Fuentes de información

Barnes, D. y Kölling, M. (2007). Programación orientada a objetos con Java (3.a ed.). Pearson Educación.

Dean, J. y Dean, R. (2009). Introducción a la programación con Java. McGraw-Hill.

Deitel P. y Deitel, H. (2008). Cómo programar en Java (7.a ed.). Pearson Educación.

Martín, A. (2008). Programador certificado Java 2 (2.a ed.). Alfaomega, Grupo Editor.


Cómo citar


Lozano, J. A. (2020). Excepciones y errores. Unidades de Apoyo para el Aprendizaje. CUAIEED/Facultad de Ingeniería-UNAM. (vínculo)