9 - Control de errores con Excepcion

Anuncio
9: El control de errores
con Excepciones
La filosofía básica de Java es que “el código mal escrito no será ejecutado.”
El momento ideal para capturar un error es en tiempo de compilación, antes
incluso de intentar ejecutar el programa. Sin embargo, no to dos los errores se
pueden detectar en tiempo de compilación. El resto de los problemas deberán
ser manejados en tiempo de ejecución, mediante alguna formalidad que
permita a la fuente del error pasar la información adecuada a un receptor que
sabrá hacerse cargo de la dificultad de manera adecuada.
En C y en otros lenguajes anteriores, podía haber varias de estas formalidades,
que generalmente se establecían por convención y no como parte del lenguaje
de programación. Habitualmente, se de- volvía un valor especial o se ponía un
indicador a uno, y el receptor se suponía, que tras echar un vistazo al valor o
al indicador, determinaría la existencia de algún problema. Sin embargo, con el
paso de los años, se descubrió que los programadores que hacían uso de
bibliotecas tendían a pensar que eran invencibles -como en "Sí, puede que los
demás cometan errores, pero no en mi código". Por tanto, y lógicamente,
éstos no comprobaban que se dieran condiciones de error (y en ocasiones las
condiciones de error eran demasiado estúpidas como para comprobarlas) [1].
Si uno fuera tan exacto como para comprobar todos los posibles errores cada
vez que se invocara a un método, su código se convertiría en una pesadilla
ilegible. Los programadores son reacios a admitir la verdad: este enfoque al
manejo de errores es una grandísima limitación especialmente de cara a la
creación de programas grandes, robustos y fácilmente mantenibles.
[1] El programador de C puede fijar el valor de retorno de printf() como un
ejemplo de esta afirmació n.
La solución es extraer del manejo de errores la naturaleza casual de los
mismos y forzar la formalidad. Esto se ha venido haciendo a lo largo de
bastante tiempo, dado que las implementaciones de manejo de excepciones se
retornan hasta los sistemas operativos de los años 60, e incluso al "error goto"
de BASIC. Pero el manejo de excepciones de C++ se basaba en Ada, y el de
Java está basado fundamentalmente en C++ (aunque se parece incluso más al
de Pascal Orientado a Objetos).
La palabra "excepción" se utiliza en el sentido de: "Yo me encargo de la
excepción a eso". En cuanto se da un problema puede desconocerse qué hacer
con el mismo, pero se sabe que simplemente no se puede continuar sin más;
hay que parar y alguien, en algún lugar, deberá averiguar qué hacer. Pero
puede que no se disponga de información suficiente en el contexto actual como
para solucionar el problema. Por tanto, se pasa el problema a un contexto
superior en el que alguien se pueda encargar de tomar la decisión adecuada
(algo semejante a una cadena de comandos).
El otro gran beneficio de las excepciones es que limpian el código de manejo
de errores. En vez de comprobar si se ha dado un error en concreto y tratar
con él en diversas partes del programa, no es necesario comprobar nada más
en el momento de invocar al método (puesto que la excepción garantizará que
alguien la capture). Y es necesario manejar el problema en un solo lugar, el
denominado gestor de excepciones. Éste salva el código, y separa el código
que describe qué se desea hacer a partir del código en ejecución cuando algo
sale mal. En general, la lectura, escritura, y depuración de código se vuelve
mucho más sencilla con excepciones, que cuando se hace uso de la antigua
manera de gestionar los errores.
Debido a que el manejo de excepciones se ve fortalecido con el compilador de
Java, hay numerosísimos ejemplos en este libro que permiten aprender todo lo
relativo al manejo de excepciones. Este capítulo presenta el código que es
necesario escribir para gestionar adecuadamente las excepciones, y la forma
de generar excepciones si algún método se mete en problemas.
Excepciones básicas
Una condición excepcional es un problema que evita la continuación de un
método o el alcance actual. Es importante distinguir una condición excepc ional
de un problema normal, en el que se tiene la suficiente información en el
contexto actual como para hacer frente a la dificultad de alguna manera. Con
una condición excepcional no se puede continuar el proceso porque no se tiene
la información necesaria para tratar el problema, en el contexto actual. Todo
lo que se puede hacer es salir del contexto actual y relegar el problema a un
contexto superior. Esto es lo que ocurre cuando se lanza una excepción.
Un ejemplo sencillo es una división. Si se corre el riesgo de dividir entre cero,
merece la pena comprobar y asegurarse de que no se seguirá adelante y se
llegará a ejecutar la división. Pero ¿qué significa que el denominador sea cero?
Quizás se sabe, en el contexto del problema que se está intentando solucionar
en ese método particular, cómo manejar un denominador cero. Pero si se trata
de un valor que no se esperaba, no se puede hacer frente a este error, por lo
que habrá que lanzar una excepción en vez de continuar hacia delante.
Cuando se lanza una excepción, ocurren varias cosas. En primer lugar, se crea
el objeto excepción de la misma forma en que se crea un objeto Java: en el
montículo, con new. Después, se detiene el cauce normal de ejecución (el que
no se podría continuar) y se lanza la referenc ia al objeto excepción desde el
contexto actual. En este momento se interpone el mecanismo de gestión de
excepciones que busca un lugar apropiado en el que continuar ejecutando el
programa. Este lugar apropiado es el gestor de excepciones, cuyo trabajo es
recuperarse del problema de forma que el programa pueda, o bien intentarlo
de nuevo, o bien simplemente continuar.
Como un ejemplo sencillo de un lanzamiento de una excepción, considérese
una referencia denominada t. Es posible que se haya recibido una re ferencia
que no se haya inicializado, por lo que sería una buena idea comprobarlo antes
de que se intentara invocar a un método utilizando esa referencia al objeto. Se
puede enviar información sobre el error a un contexto mayor creando un
objeto que represente la información y "arrojándolo" fuera del contexto actual.
A esto se le llama lanzamiento de una excepción. Tiene esta apariencia:
if(t == null)
throw new NullPointerException();
Esto lanza una excepción, que permite - en el contexto actual - abdicar la
responsabilidad de pensar sobre este aspecto más adelante. Simplemente se
gestiona automáticamente en algún otro sitio. El dónde se mostrará más tarde.
Parámetros de las excepciones
Como cualquier otro objeto en Java, las excepciones siempre se crean en el
montículo haciendo uso de new, que asigna espacio de almacenamiento e
invoca a un constructor. Hay dos constructores en todas las excepciones
estándar: el primero es el constructor por defecto y el segundo toma un
parámetro string de forma que se pueda ubicar la información pertinente en la
excepción:
throw new NullPointerException("t = null");
Este string puede extraerse posteriormente utilizando varios métodos, como se
verá más adelante.
La palabra clave throw hace que ocurran varias cosas relativamente mágicas.
Habitualmente, se usará primero new para crear un objeto que represente la
condición de error. Se da a throw la referencia resultante. En efecto, el
método "devuelve" el objeto, incluso aunque ese tipo de objeto no sea el que
el método debería devolver de forma natural. Una manera natural de pensar
en las excepciones es como si se tratara de un mecanismo de retorno
alternativo, aunque el que lleve esta analogía demasiado lejos acabará
teniendo problemas. También se puede salir del ámbito ordinario lanzando una
excepción. Pero se devuelve un valor, y el método o el ámbito finalizan.
Cualquier semejanza con un método de retorno ordinario acaba aquí, puesto
que el punto de retorno es un lugar completamente diferente del punto al que
se sale en una llamada normal a un método. (Se acaba en un gestor de
excepciones adecuado que podría estar a cientos de kilómetros – es decir,
mucho más bajo dentro de la pila de invocaciones – del punto en que se lanzó
la excepción.)
Además, se puede lanz ar cualquier tipo de objeto lanzable Throwable que se
desee. Habitualmente, se lanzará una clase de excepción diferente para cada
tipo de error. La información sobre cada error se representa tanto dentro del
objeto excepción como implícitamente en el tipo de objeto excepción elegido,
puesto que alguien de un contexto superior podría averiguar qué hacer con la
excepción. (A menudo, la única información es el tipo de objeto excepción, y
no se almacena nada significativo junto con el objeto excepción.)
Capturar una excepción
Si un método lanza una excepción, debe asumir que esa excepción será
"capturada" y que será tratada. Una de las ventajas del manejo de excepciones
de Java es que te permite concentrarte en el problema que se intenta
solucionar en un único sitio, y tratar los errores que ese código genere en otro
sitio.
Para ver cómo se captura una excepción, hay que entender primero el
concepto de región guardada, que es una sección de código que podría
producir excepciones, y que es seguida del código que maneja esas
excepciones.
El bloque try
Si uno está dentro de un método y lanza una excepción (o lo hace otro método
al que se invoque), ese método acabará en el momento en que haga el
lanzamiento. Si no se desea que una excepción implique abandonar un
método, se puede establecer un bloque especial dentro de ese método para
que capture la excepción. A este bloque se le denomina el bloque try puesto
que en él se "intentan" varias llamadas a métodos. El bloque try es un ámbito
ordinario, precedido de la palabra clave try:
try {
// el código que podría generar excepciones
}
Si se estuviera comprobando la existencia de errores minuciosamente en un
lenguaje de programación que no soporte manejo de excepciones, habría que
rodear cada llamada a método con código de prueba de invocación y errores,
incluso cuando el mismo método fuese invocado varias veces. Esto significa
que el código es mucho más fácil de escribir y leer debido a que no se
confunde el objetivo del código con la comprobación de errores.
Manejadores de excepción
Por supuesto, la excepción que se lance debe acabar en algún sitio. Este "sitio"
es el manejador de excepciones y hay uno por cada tipo de excepción que se
desee capturar. Los manejadores de ex cepciones siguen inmediatamente al
bloque try y se identifican por la palabra clave catch:
try {
// el código que podría generar excepciones
} catch(Type1 id1) {
// maneja excepciones de Type1
} catch(Type2 id2) {
// maneja excepciones de Type2
} catch(Type3 id3) {
// maneja excepciones d e Type3
}
// etc...
Cada cláusula catch (manejador de excepciones) es semejante a un pequeño
método que toma uno y sólo un argumento de un tipo en particular. El
identificador (id1, id2, y así sucesivamente) puede usarse dentro del
manejador, exactamente igual que un parámetro de un método. En ocasiones
nunca se usa el identificador porque el tipo de la excepción proporciona la
suficiente información como para tratar la excepción, pero el identificador debe
seguir ahí.
Los manejadores deben aparecer directamente tras el bloque try. Si se lanza
una excepción, el mecanismo de gestión de excepciones trata de cazar el
primer manejador con un argumento que coincida con el tipo de excepción.
Posteriormente, entra en esa cláusula catch, y la excepción se da por
manejada. La búsqueda de manejadores se detiene una vez que se ha
finalizado la cláusula catch. Sólo se ejecuta la cláusula catch; no es como una
sentencia switch en la que haya que colocar un break después de cada case
para evitar que se ejecute el resto.
Fíjese que, dentro del bloque try , varias llamadas a métodos podrían generar
la misma excepción, pero sólo se necesita un manejador.
Terminación vs. Reanudación
Hay dos modelos básicos en la teoría de manejo de excepciones. En la
terminación (que es lo que soportan Java y C++) se asume que el error es tan
crítico que no hay forma de volver atrás a resolver dónde se dio la excepción.
Quien quiera que lanzara la excepción decidió que no había forma de resolver
la situación, y no quería volver atrás.
La alternativa es el reanudación. Significa que se espera que el manejador de
excepciones haga algo para rectificar la situación, y después se vuelve a
ejecutar el método que causó el error, presumiendo que a la segunda no
fallará. Desear este segundo caso significa que se sigue pensando que la
excepción continuará tras el manejo de la excepción. En este caso, la
excepción es más como una llamada a un método - que es como deberían
establecerse en Java aquellas situaciones en las que se desea este tipo de
comportamiento. (Es decir, es mejor llamar a un método que solucione el
problema antes de lanzar una excepción.) Alternativamente, se ubica el bloque
try dentro de un bucle while que sigue intentando volver a entrar en el bloque
try hasta que se obtenga el resultado satisfactorio.
Históricamente, los programadores que usaban sistemas operativos que
soportaban el manejo de excepciones reentrantes acababan usando en su
lugar código con terminación. Por tanto, aunque la técnica de los reintentos
parezca atractiva a primera vista, no es tan útil en la práctica. La razón
dominante es probablemente el acoplamiento resultante: el manejador debe
ser, a menudo, consciente de dónde se lanza la excepción y contener el código
no genérico específico del lugar de lanzamiento. Esto hace que el código sea
difícil de escribir y mantener, especialmente en el caso de sistemas grandes en
los que la excepción podría generarse en varios puntos.
Crear sus propias excepciones
No hay ninguna limitación que obligue a utilizar las excepciones existentes en
Java. Esto es importante porque a menudo será necesario crear sus propias
excepciones para indicar un error especial que puede crear su propia
biblioteca, pero que no fue previsto cuando se creó la jerarquía de excepciones
de Java.
Para crear su propia clase excepción, se verá obligado a heredar de un tipo de
excepción existente, preferentemente uno cercano al significado de su nueva
excepción (sin embargo, a menudo esto no es posible). La forma más trivial de
crear un nuevo tipo de excepción es simplemente dejar que el compilador cree
el constructor por defecto, de forma que prácticamente no haya que escribir
ningún código:
//: c09:SimpleExceptionDemo.java
// heredando tus excepciones.
import com.bruceeckel.simpletest.*;
class SimpleException extends Exception {}
public class SimpleExceptionDemo {
private static Test monitor = new Test();
public void f() throws SimpleException {
System.out.println("Lanza SimpleException desde f()");
throw new SimpleException();
}
public static void main(String[] args) {
SimpleExceptionDemo sed = new SimpleExceptionDemo();
try {
sed.f();
} catch(SimpleException e) {
System.err.println( "Lo atrapé!");
}
monitor.expect(new String[] {
"Atraviesa SimpleException desde f()",
"Lo atrapé!"
});
}
} ///:~
Cuando el compilador crea el constructor por defecto, se trata del que llama
automáticamente (y de forma invisible) al constructor por defecto de la clase
base. Por supuesto, en este caso no se obtendrá un constructor
SimpleException(String), pero en la práctica esto no se usa mucho. Como
se verá, lo más importante de una excepción es el nombre de la clase, por lo
que en la mayoría de ocasiones una excepción como la mostrada arriba es
plenamente satisfactoria.
Aquí, se imprime el resultado en la consola de error estándar escribiendo en
System.err . Éste suele ser el mejor sitio para enviar información de error, en
vez de System.out , que podría estar redirigida. Si se envía la salida a
System.err n o estará redireccionada junto con System.out, por lo que el
usuario tiene más probabilidades de enterarse.
Crear una clase excepción que tenga también un constructor que tome un
String como parámetro es bastante sencillo:
//: c09:FullConstructors.java
import com.bruceeckel.simpletest.*;
class MyException extends Exception {
public MyException() {}
public MyException(String msg) { super (msg); }
}
public class FullConstructors {
private static Test monitor = new Test();
public static void f() throws MyException {
System.out.println("Lanzando MyException desde f()" );
throw new MyException();
}
public static void g() throws MyException {
System.out.println("Lanzando MyException desde g()" );
throw new MyException("Originado en g()");
}
public static void main(String[] args) {
try {
f();
} catch(MyException e) {
e.printStackTrace();
}
try {
g();
} catch(MyException e) {
e.printStackTrace();
}
monitor.expect(new String[] {
"Lanzando MyException desde f()",
"MyException",
"%% \thasta FullConstructors.f\\(.*\\)",
"%% \thasta FullConstructors.main\\(.*\\)",
"Lanzando MyException desde g()",
"MyException: Originado en g()",
"%% \thasta FullConstructors.g\\(.*\\)",
"%% \thasta FullConstructors.main\\(.*\\)"
});
}
} ///:~
El código añadido es poco: - la inserción de dos constructores que definen la
forma de crear MyException. En el segundo constructor, el constructor de
clase base con un argumento String es explícitamente invocado usando la
palabra clave super.
En los manipuladores, uno de los métodos Throwable (de cuál Exceptio n es
heredada) es llamado: PrintStackTrace( ). Esto produce información acerca
de la secuencia de métodos que fueron llamados para ponerse directos donde
la excepción ocurrió. Por defecto, la información va al flujo estándar de error,
pero las versiones sobrecargadas te permiten enviar los resultados a cualquier
otro flujo igualmente.
El proceso de crear tus excepciones puede ser tomado más allá. puedes
agregar constructores y miembros extras:
//: c09:ExtraFeatures.java
// Futuro embellecimiento de clases de excepción.
import com.bruceeckel.simpletest.*;
class MyException2 extends Exception {
private int x;
public MyException2() {}
public MyException2(String msg) { super(msg); }
public MyException2(String msg, int x) {
super(msg);
this.x = x;
}
public int val() { return x; }
public String getMessage() {
return "Mensaje Detallado: "+ x + " "+ super.getMessage();
}
}
public class ExtraFeatures {
private static Test monitor = new Test();
public static void f() throws MyException2 {
System.out.println("Lanzando MyException2 desde f()");
throw new MyException2();
}
public static void g() throws MyException2 {
System.out.println("Lanzando MyException2 desde g()");
throw new MyException2("Originado en g()" );
}
public static void h() throws MyException2 {
System.out.println("Lanzando MyException2 desde h()");
throw new MyException2("Originado en h()" , 47);
}
public static void main(String[] args) {
try {
f();
} catch(MyException2 e) {
e.printStackTrace();
}
try {
g();
} catch(MyException2 e) {
e.printStackTrace();
}
try {
h();
} catch(MyException2 e) {
e.printStackTrace();
System.err.println( "e.val() = " + e.val());
}
monitor.expect(new String[] {
"Lanzando MyException2 desde f()",
"MyException2: Mensaje Detallado: 0 null",
"%% \thasta ExtraFeatures.f\\(.*\\)",
"%% \thasta ExtraFeatures.main\\(.*\\)",
"Lanzando MyException2 desde g()",
"MyException2: Mensaje Detallado: 0 Originado en g()",
"%% \thasta ExtraFeatures.g\\(.*\\)",
"%% \thasta ExtraFeatures.main\\(.*\\)",
"Lanzando MyException2 desde h()",
"MyException2: Mensaje Detallado: 47 Originado en h()" ,
"%% \thasta ExtraFeatures.h\\(.*\\)",
"%% \thasta ExtraFeatures.main\\(.*\\)",
"e.val() = 47"
});
}
} ///:~
Un campo i ha sido añadido, junto con un método que lee ese valor y un
constructor adicional que lo coloca. Además, Throwable.getMessage ( ) ha
sido sobrescrito para producir un mensaje detallado. getMessage ( ) es algo
como toString ( ) para clases de excepción.
Desde que una excepción es simplemente otra clase de objeto, puedes
continuar este proceso de embelleciendo el poder de tus clases de excepción.
Mantén en mente, sin embargo, tan todo este juego de disfraces podría ser
despistado en los programadores del cliente usando tus paquetes, desde que
simplemente podrían buscar la excepción para ser lanzados y nada más. (Es el
método más de las excepciones de la biblioteca Java que son usados.)
Especificación de excepción
En Java, se pide que se informe al programador cliente, que llama al método,
de las excepciones que podría lanzar ese método. Esto es bastante lógico
porque el llamador podrá saber exactamente el código que debe escribir si
desea capturar todas las excepciones potenciales. Por supuesto, si está
disponible el código fuente, el programador cliente podría simplemente buscar
sentencias throw, pero a menudo las bibliotecas no vienen con sus fuentes.
Para evitar que esto sea un problema, Java proporciona una sintaxis (y fuerza
el uso de la misma) para permitir decir educadamente al programador cliente
qué excepciones lanza ese método, de forma que el programador cliente pueda
manejarlas. Ésta es la especificación de excepciones, y es parte de la
declaración del método, y se sitúa justo después de la lista de parámetros.
La especificación de excepciones utiliza la palabra clave throws, seguida de la
lista de todos los tipos de excepción potenciales. Por tanto, la definición de un
método podría tener la siguiente apariencia:
void f() throws TooBig, TooSmall, DivZero { //...
Si se dice
void f() { // ...
significa que el método no lanza excepciones. (Excepto las excepciones de tipo
RuntimeException, que puede ser lanzado razonablemente desde cualquier
sitio -como se describirá más adelante.)
No se puede engañar sobre una especificación de excepciones - si un método
provoca excepciones y no las maneja, el compilador lo detectará e indicará
que, o bien hay que manejar la excepción o bien hay que indicar en la
especificación de excepciones todas las excepciones que el método pue de
lanzar. Al fortalecer las especificaciones de excepciones de arriba abajo, Java
garantiza que se puede asegurar la corrección de la excepción en tiempo de
compilación [2].
[2] Esto constituye una mejo ra significativa frente al manejo de excepciones
de C++, que no captura posibles violaciones de especificaciones de
excepciones hasta tiempo de ejecución, donde no es ya muy útil.
Sólo hay un lugar en el que se pude engañar: se puede decir que se lanza una
excepción que verdaderamente no se lanza. El compilador cree en tu palabra,
y fuerza a los usuarios del método a tratarlo como si verdaderamente arrojara
la excepción. Esto tiene un efecto beneficioso al ser un objeto preparado para
esa excepción, de forma que, de hecho, se puede empezar a lanzar la
excepción más tarde sin que esto requiera modificar el código ya existente.
También es importante para la creació n de clases base abstractas e interfaces
cuyas clases derivadas o implementaciones pueden necesitar lanzar
excepciones.
Las excepciones que son comprobadas y obligadas en la fase de compilación
son llamadas excepciones comprobadas.
Capturar cualquier excepción
Es posible crear un manejador que capture cualquier tipo de excepción. Esto se
hace capturando la excepción de clase base Exception (hay otros tipos de
excepciones base, pero Exception es la clase base a utilizar pertinentemente
en todas las actividades de programación):
catch(Exception e) {
System.err.println("Capturé una excepción");
}
Esto capturará cualquier excepción, de forma que si se usa, habrá que ponerlo
al final de la lista de manejadores para evitar que los manejadores de
excepciones que puedan venir después queden ignorados.
Dado que la clase Exception es la base de todas las clases de excepción que
son importantes para el programador, no se logra mucha información
específica sobre la excepción, pero se puede llamar a los métodos que vienen
de su tipo base Throwable:
String getMessage ( )
String getLocalizedMessage ( )
Toma el mensaje de detalle, o un mensaje ajustado a este escenario
particular.
String toString ( )
Devuelve una breve descripción del objeto Throwable , incluyendo el mensaje
de detalle si es que lo hay.
void printStackTrace ( )
void printStackTrace ( PrintStream )
void printStackTrace ( java.io.PrintWriter )
Imprime el objeto y la traza de pila de llamadas lanzada. La pila de llamadas
muestra la secuencia de llamadas al método que condujeron al momento en
que se lanzó la excepción. La primera versión imprime en el error estándar, la
segunda y la tercera apuntan a un flujo de datos de tu elección (en el Capítulo
12, se entenderá por qué hay dos tipos de flujo de datos).
Throwable fillInStackTrace ( )
Registra información dentro de este objeto Throwable, relativa al estado
actual de las pilas. Es útil cuando una aplicación está relanzando un error o
una excepción (en breve se contará algo más al respecto).
Además, se pueden conseguir otros métodos del tipo base de Throwable,
Object (que es el tipo base de todos). El que podría venir al dedillo para
excepciones es getClass() que devuelve un objeto que representa la clase de
este objeto. Se puede también preguntar al objeto de esta Clase por su
nombre haciendo uso de getName() o toString(). Asimismo se pueden hacer
cosas más sofisticadas con objetos Class que no son necesarios en el manejo
de excepciones. Los objetos Class se estudiarán más adelante.
He aquí un ejemplo que muestra el uso de los métodos básicos de Exception:
//: c09:ExceptionMethods.java
// Demostrando los Métodos de Excepción.
import com.bruceeckel.simpletest.*;
public class ExceptionMethods {
private static Test monitor = new Test();
public static void main(String[] args) {
try {
throw new Exception("Mi Excep cion" );
} catch(Exception e) {
System.err.println( "Capture la Excepcion");
System.err.println( "getMessage():" + e.getMessage());
System.err.println( "getLocalizedMessage():" +
e.getLocalizedMessage());
System.err.println( "toString():" + e);
System.err.println( "printStackTrace():" );
e.printStackTrace();
}
monitor.expect(new String[] {
"Capture la Excepcion",
"getMessage():Mi Excepcion",
"getLocalizedMessage():M i Excepcion",
"toString():java.lang.Exception: M i Excepcion",
"printStackTrace():",
"java.lang.Exception: Mi Excepcion",
"%% \thasta ExceptionMethods.main\\(.*\\)"
});
}
} ///:~
Se puede ver que los métodos proporcionan más información exitosamente cada una es efectivamente, un superconjunto de la anterior.
Relanzar una excepción
En ocasiones se desea volver a lanzar una excepción que se acaba de capturar,
especialmente cuando se usa Exception para capturar cualquier excepción.
Dado que ya se tiene la referencia a la excepción actual, se puede volver a
lanzar esa referencia:
catch(Exception e) {
System.err.println("Una excepción fue lanzada");
throw e;
}
Volver a lanzar una excepción hace que la excepción vaya al contexto
inmediatamente más alto de manejadores de excepciones. Cualquier cláusula
catch subsiguiente del mismo bloque try seguirá siendo ignorada. Además, se
preserva todo lo relativo al objeto, de forma que el contexto superior que
captura el tipo de excepción específico pueda extraer toda la información de
ese objeto.
Si simplemente se vuelve a lanzar la excepción actual, la información que se
imprime sobre esa excepción en printStackTrace( ) estará relacionada con el
origen de la excepción, no al lugar en el que se volvió a lanzar. Si se desea
instalar nue va información de seguimiento de la pila, se puede lograr mediante
fillInStackTrace( ), que devuelve un objeto Throwable creado rellenando la
información de la pila actual en el antiguo objeto excepción. Éste es su
aspecto:
//: c09:Rethrowing.java
// Demonstrando fillInStackTrace()
import com.bruceeckel.simpletest.*;
public class Rethrowing {
private static Test monitor = new Test();
public static void f() throws Exception {
System.out.println("originando la excepcion en f()" );
throw new Exception("lanzado desde f()");
}
public static void g() throws Throwable {
try {
f();
} catch(Exception e) {
System.err.println( "En g(),e.printStackTrace()" );
e.printStackTrace();
throw e; // 17
// atraviesa e.fillInStackTrace(); // 18
}
}
public static void
main(String[] args) throws Throwable {
try {
g();
} catch(Exception e) {
System.err.println(
"Capturado e n main, e.printStackTrace()");
e.printStackTrace();
}
monitor.expect(new String[] {
"originando la excepcion en f()",
"En g(),e.printStackTrace()",
"java.lang.Exception: lanzado desde f()",
"%% \thasta Rethrowing.f(.*?)",
"%% \thasta Rethrowing.g(.*?)",
"%% \thasta Rethrowing.m ain(.*?)",
"Capturado en main, e.printStackTrace()",
"java.lang.Exception: lanzado desde f()",
"%% \thasta Rethrowing.f(.*?)",
"%% \thasta Rethrowing.g(.*?)",
"%% \thasta Rethrowing.main(.*?)"
});
}
} ///:~
Los números importantes de la línea son marcados como comentarios. Con la
línea 17 no comentada (como mostrado), se muestra la salida, así es que el
rastro de la pila de excepción siempre recuerda su punto de origen verdadero
no importa cuán muchas veces queda relanzado.
Considerando que la línea 17 sea comentario, y no lo sea la línea 18, se usa
fillInStackTrace(), s iendo el resultado:
originando la exception in f()
En g(),e.printStackTrace()
java.lang.Exception: lanzado desde f()
hasta Rethrowing.f(Rethrowi ng.java:9)
hasta Rethrowing.g(Rethrowing.java:12)
hasta Rethrowing.main(Rethrowing.java:23)
Capturado en main, e.printStackTrace()
java.lang.Exception: lanzado desde f()
hasta Rethrowing.g(Rethrowing.java:18)
hasta Rethrowing.main(Rethrowing.java:23)
(Agregue complementos adicionales desde el método Test.expect( ).) Por
fillInStackTrace(), la línea 18 se convierte en el punto de origen nuevo de la
excepción.
La clase Throwable debe aparecer en la especificación de excepción para g()
y main() porque fillInStackTrace() produce una referencia a un objeto
Throwable . Desde que Throwable es una clase base de Exception , es
posible obtener un objeto que es un Throwable pero no un Exception, así es
que el manipulador para Exception en main() le podría hacer falta. Para estar
seguro de que todo está en orden, el compilador fuerza una especificación de
excepción para Throwable . Por ejemplo, la excepción en el siguiente
programa no es capturada en main():
//: c09:ThrowOut.java
// {ThrowsException}
public class ThrowOut {
public static void
main(String[] args) throws Throwable {
try {
throw new Throwable();
} catch(Exception e) {
System.err.println( "Capturado en main()");
}
}
} ///:~
También es posible volver a lanzar una excepción diferente de la capturada. Si
se hace esto, se consigue un efecto similar al usar fillInStackTrace( ) -la
información sobre el origen primero de la excepción se pierde, y lo que queda
es la información relativa al siguiente throw:
//: c09:RethrowNew.java
// Relanza un objeto diferente desde el primero que
// fue capturado.
// {ThrowsException}
import com.bruceeckel.simpletest.*;
class OneException extends Exception {
public OneException(String s) { super(s); }
}
class TwoException extends Exception {
public TwoException(String s) { super(s); }
}
public class RethrowNew {
private static Test monitor = new Test();
public static void f() throws OneException {
System.out.println("originando la excepcion en f()" );
throw new OneException("lanzado desde f()");
}
public static void
main(String[] args) throws TwoException {
try {
f();
} catch(OneException e) {
System.err.println(
"Capturado en main, e.printStackTrace()");
e.printStackTrace();
throw new TwoException("desde main()");
}
monitor.expect(new String[] {
"originando la excepcion en f ()",
"Capturado en main, e.printStackTrace()",
"OneException: lanzado desde f()",
"\thasta RethrowNew.f(RethrowNew.java:18)",
"\thasta RethrowNew.main(RethrowNew.java:22)",
"Excepcion en hilo \"main\" " +
"TwoException: desde main()",
"\thasta RethrowNew.main(RethrowNew.java:28)"
});
}
} ///:~
La excepción final sólo sabe que proviene de main(), y no de f().
No hay que preocuparse nunca de limpiar la excepción previa, o cualquier otra
excepción en este sentido. Todas son objetos basados en el montículo creados
con new, por lo que el recolector de basura los limpia automáticamente.
Encadenamiento de excepción
A menudo
quieres capturar una excepción y lanzar otro, pero todavía
mantiene la información acerca de la excepción originaria – esto es llamado
encadenamiento de excepción. Antes de JDK 1.4, los programadores tuvieron
que escribir su código para conservar la información original de excepción,
pero ahora todas las subclases Throwable pueden llevar un objeto de causa
en su constructor. La causa está dirigida a ser la excepción originaria, y
pasando eso en ti mantiene el rastro de la pila de regreso a su origen, aun
cuando estás generando y lanzando una excepción nueva en este punto.
Es interesante notificar que las únicas subclases Throwable que proveen el
argumento de causa en el constructor son las tres clases excepción
fundamental Error (usadas por el JVM para reportar errores de sistema),
Exception, y RuntimeException. Si
quieres concatenar cualquier tipo
distintos de excepción, lo haces a través del método initCause() en vez del
constructor.
Aquí hay un ejemplo que te permite dinámicamente añadirle los campos a un
objeto DynamicFields en el tiempo de ejecución:
//: c09:DynamicFields.java
// Una Clase que dinamicamente agrega campos a si mismo .
// Demuestra encadenamiento de excepcion .
// {ThrowsException}
import com.bruceeckel.simpletest.*;
class DynamicFieldsException extends Exception {}
public class DynamicFields {
private static Test monitor = new Test();
private Object[][] fields;
public DynamicFields(int initialSize) {
fields = new Object[initialSize][2];
for(int i = 0; i < initialSize; i++)
fields[i] = new Object[] { null, null };
}
public String toString() {
StringBuffer result = new StringBuffer();
for(int i = 0; i < fields.length; i++) {
result.append(fields[i][0]);
result.append( ": ");
result.append(fields[i][1]);
result.append( "\n");
}
return result.toString();
}
private int hasField(String id) {
for(int i = 0; i < fields.length; i++)
if(id.equals(fields[i][0]))
return i;
return -1;
}
private int
getFieldNumber(String id) throws NoSuchFieldException {
int fieldNum = hasField(id);
if(fieldNum == -1)
throw new NoSuchFieldException();
return fieldNum;
}
private int makeField(String id) {
for(int i = 0; i < fields.length; i++)
if(fields[i][0] == null) {
fields[i][0] = id;
return i;
}
// Sin campos vacios. Agrega uno:
Object[][]tmp = new Object[fields.length + 1][2];
for(int i = 0; i < fields.length; i++)
tmp[i] = fields[i];
for(int i = fields.length; i < tmp.length; i++)
tmp[i] = new Object[] { null, null };
fields = tmp;
// Llamada reursiva con campos expandidos :
return makeField(id);
}
public Object
getField(String id) throws NoSuchFieldException {
return fields[getFieldNumber(id)][1];
}
public Object setField(String id, Object value)
throws DynamicFieldsException {
if(value == null ) {
// Muchas excepciones no tienen un constructor de
// “causa”. En estos casos debes usar initCause(),
// disponibles en todas las subclases de Throwable.
DynamicFieldsException dfe =
new DynamicFieldsException();
dfe.initCause( new NullPointerException());
throw dfe;
}
int fieldNumber = hasField(id);
if(fieldNumber == -1)
fieldNumber = makeField(id);
Object result = null;
try {
result = getField(id); // Obtiene el valor viejo
} catch(NoSuchFieldException e) {
// Usa el constructor que toma la "caus a":
throw new RuntimeException(e);
}
fields[fieldNumber][1] = value;
return result;
}
public static void main(String[] args) {
DynamicFields df = new DynamicFields(3);
System.out.println(df);
try {
df.setField("d", "A value for d");
df.setField("number", new Integer(47));
df.setField("number2", new Integer(48));
System.out.println(df);
df.setField("d", "A new value for d");
df.setField("number3", new Integer(11));
System.out.println( df);
System.out.println(df.getField("d" ));
Object field = df.getField("a3"); // Excepcion
} catch(NoSuchFieldException e) {
throw new RuntimeException(e);
} catch(DynamicFieldsException e) {
throw new RuntimeException(e);
}
monitor.expect(new String[] {
"null: null",
"null: null",
"null: null",
"",
"d: Un valor para d",
"numero: 47",
"numero 2: 48" ,
"",
"d: Un nuevo valor para d",
"numero: 47",
"numero2: 48",
"numero3: 11",
"",
"Un valor para d",
"Excepcion en hilo \"main\" " +
"java.lang.RuntimeException: " +
"java.lang.NoSuchFieldException",
"\thasta DynamicFields.main(DynamicFields.java:98)",
"Causado por: java.lang.NoSuchFieldException",
"\thasta DynamicFields.getFieldNumber(" +
"DynamicFields.java:37)" ,
"\thasta DynamicFields.getField(DynamicFields.java:58)",
"\thasta DynamicFields.main(DynamicFields.java:96)"
});
}
} ///:~
Cada objeto DynamicFields contiene un arreglo de pares de Objeto-Objeto. El
primer objeto es el identificador del campo (un String), y el segundo es el
valor del campo, lo cual puede ser cualquier tipo excepto un primitivo
desenvuelto. Cuando usted crea el objeto, usted hace una suposición educada
de aproximadamente cuántos campos a usted le hace falta. Cuando usted
llama a setField(), este o encuentra el campo existente por ese nombre o crea
a uno nuevo, y aporta su valor. Si corre fuera de espacio, suma espacio nuevo
creando un arreglo de longitud una más largo y copiando los viejos elementos
adentro. Si usted trata de aportar un valor nulo, entonces lanza a un
DynamicFieldsException creando uno y usando a initCause() para insertar
a un NullPointerException como la causa.
Como un valor de retorno, setField() también hace salir el valor viejo en esa
posición del campo usando a getField(), lo cual podría lanzar a un
NoSuchFieldException. Si el programador cliente llama a getField(), luego
son responsables de manipular a NoSuchFieldException , pero si esta
excepción es lanzado dentro de setField (), es un error de programación, así
es que el NoSuchFieldException es convertido a un RuntimeException
usando al constructor que toma un el argumento de causa.
Excepciones estándar Java
La clase Throwable de Java describe todo aquello que se pueda lanzar en
forma de excepción. Hay dos tipos generales de objetos Throwable ("tipos
de" - "heredados de"). Error representa los errores de sistema y de tiempo de
compilación que uno no se preocupa de capturar (excepto en casos
especiales). Exception es el tipo básico que puede lanzarse desde cualquier
método de las clases de la biblioteca estándar de Java y desde los métodos
que uno elabore, además de incidencias en tiempo de ejecución. Por tanto, el
tipo base que más interesa al programador es Exception .
La mejor manera de repasar las excepciones es navegar por la documentación
HTML de Java que se puede descargar de jaua.sun.com. Merece la pena hacer
esto simplemente para to mar un contacto inicial con las diversas excepciones,
aunque pronto se verá que no hay nada especial entre las distintas
excepciones, exceptuando el nombre. Además, Java cada vez tiene más
excepciones; básicamente no tiene sentido imprimirlas en un libro. Cualquier
biblioteca nueva que se obtenga de un tercero probablemente tendrá también
sus propias excepciones. Lo que es importante entender es el concepto y qué
es lo que se debe hacer con las excepciones.
La idea básica es que el nombre de la excepción representa el problema que ha
sucedido; de hecho se pretende que el nombre de las excepciones sea
autoexplicativo. Las excepciones no están todas ellas definidas en java.lang;
algunas están creadas para dar soporte a otras bibliotecas como util, net e io.
Así, por ejemplo, todas las excepciones de E/S se heredan de
java.io.IOException.
El caso especial de RuntimeException
El primer ejemplo en este capítulo fue
if(t == null)
throw new NullPointerException();
Puede ser bastante horroroso pensar que hay que comprobar que cualquier
referencia que se pase a un método sea null o no null (de hecho, no se puede
saber si el que llama pasa una referencia válida). Afortunadamente, no hay
que hacerlo - esto es parte de las comprobaciones estándares de tiempo de
ejecución que hace Java, de forma que si se hiciera una llamada a una
referencia null, Java lanzaría automáticamente una NullPointerException.
Por tanto, el fragmento de código de arriba es totalmente superfluo.
Hay un grupo completo de tipos de excepcio nes en esta categoría. Se trata de
excepciones que Java siempre lanza automáticamente, y no hay que incluirlas
en las especificaciones de excepciones. Además, están convenientemente
agrupadas juntas bajo una única clase base denominada RuntimeException,
que es un ejemplo perfecto de herencia: establece una familia de tipos que
tienen algunas características Y comportamientos en común. Tampoco se
escribe nunca una especificación de excepciones diciendo que un método
podría lanzar una RuntimeException, puesto que se asume. Dado que
indican fallos, generalmente una RuntimeException nunca se captura – se
maneja automáticamente. Si uno se viera forzado a comprobar las excepciones
de tipo RuntimeException, su código se volvería farragoso. Incluso aunque
generalmente las RuntimeExceptions no se capturan, en los paquetes que
uno construya se podría decidir lanzar algunas RuntimeExceptions.
¿Qué ocurre si estas excepciones no se capturan? Dado que el compilador no
fortalece las especificaciones de excepciones en este caso, es bastante
verosímil que una RuntimeException pudiera filtrar todo el camino hacia el
exterior hasta el método main() sin ser capturada. Para ver qué ocurre en
este caso, puede probarse el siguiente ejemplo:
//: c09:NeverCaught.java
// Ignorando RuntimeExceptions.
// {ThrowsException}
import com.bruceeckel.simpletest.*;
public class NeverCaught {
private static Test monitor = new Test();
static void f() {
throw new RuntimeException("From f()");
}
static void g() {
f();
}
public static void main(String[] args) {
g();
monitor.expect(new String[] {
"Excepcion en hilo \"main\" " +
"java.lang.RuntimeException: desde f()" ,
"
hasta NeverCaught.f(NeverCaught.java:7)" ,
"
hasta NeverCaught.g(NeverCaught.java:10)",
"
hasta NeverCaught.main(NeverCaught.java:13)"
});
}
} ///:~
Ya se puede ver que una RuntimeException (y cualquier cosa que se herede
de la misma) es un caso especial, puesto que el compilador no exige una
especificación de excepciones para ellas.
Por tanto la respuesta es: si una excepción en tiempo de ejecución
consigue todo el camino hasta el método main() sin ser capturada, se invoca a
printStackTrace() para esa excepción, y el programa finaliza su ejecución.
Debe tenerse en cuenta que sólo se pueden ignorar en un código propio las
excepciones en tiempo de ejecución, puesto que el compilador obliga a
realizar el resto de gestiones. El razonamiento es que una excepción en
tiempo de ejecución representa un error de programación:
1. Un error que no se puede capturar (la recepción de una referencia null proveniente
de un programador cliente por parte de un método, por ejemplo).
2. Un error que uno, como programador, debería haber comprobado en su código
(como un ArrayIndexOutOfBoundsException en la que se debería haber
comprobado el tamaño del array) Una excepción que ocurre desde el punto #1 a
menudo se convierte en un asunto para el punto #2.
Se puede ver fácilmente el gran beneficio aportado por estas excepciones,
puesto que éstas ayudan en el proceso de depuración.
Es interesante saber que no se puede clasificar el manejo de excepciones de
Java como si fuera una herramienta de propósito específico. Aunque
efectivamente está diseñado para manejar estos errores de tiempo de
compilación que se darán por motivos externos al propio código, simplemente
es esencial para determinados tipos de fallos de programación que el
compilador no pueda detectar.
Limpiando con finally
Hay a menudo algunas pieza de código que usted quiere ejecutar si o no una
excepción es lanzada dentro de un bloque try . Esto usualmente pertenece para
alguna operación aparte de la recuperación de memoria (desde que eso ha
tenido cuidado de por el colector de basuras). Para lograr este efecto, usted
usa una cláusula finally [2] al final de todos los manipuladores de excepción.
El esquema completo de una sección de manejo de excepción es por
consiguiente:
[3] El manejo de excepciones de C++ no tiene a
l cláusula finally porque
confía en los destructores para complementar ese tipo de limpieza.
try {
// La region protegida: Actvidades Peligrosas
// que podria lanzar A, B, o C
} catch(A a1) {
// Manipulador para situation A
} catch(B b1) {
// Manipulador para situation B
} catch(C c1) {
// Manipulador para situation C
} finally {
// Actividades que ocurren siempre
}
Para demostrar que la cláusula finally siempre se ejecuta, pruebe este
programa:
//: c09:FinallyWorks.java
// La clausula finally es siempre ejecut ado.
import com.bruceeckel.simpletest.*;
class ThreeException extends Exception {}
public class FinallyWorks {
private static Test monitor = new Test();
static int count = 0;
public static void main(String[] args) {
while(true) {
try {
// Post-increment o es cero la primera vez:
if(count++ == 0)
throw new ThreeException();
System.out.println("No exception");
} catch(ThreeException e) {
System.err.println("ThreeException");
} finally {
System.err.println("In finally clause");
if(count == 2) break; // fuera del "while"
}
}
monitor.expect(new String[] {
"ThreeException",
"En clausula finally",
"Sin exception",
"En clausula finally"
});
}
} ///:~
De la salida, usted puede ver que ya sea o no una excepción es lanzada, la
cláusula finally está todo el tiempo ejecutada.
Este programa también da una orientación para manejar el hecho de que las
excepciones de Java (al igual que ocurre en C++) no permiten volver a
ejecutar a partir del punto en que se lanzó la excepción, como ya se comentó.
Si se ubica el bloque try dentro de un bloque, se podría establecer una
condición a alcanzar antes de continuar con el programa. También se puede
añadir un contador estático o algún otro dispositivo para permitir al bucle
intentar distintos enfoques antes de rendirse. De esta forma se pueden
construir programas de extremada fortaleza.
¿Para qué sirve finaly?
En un lenguaje sin colección de basura y sin llamadas automáticas del
destructor, [4] finally es importante porque le permite al programador
garantizar que la liberación de memoria a pesar de lo que ocurre en el bloque
try. Pero Java tiene colección de basura, así es que la memoria liberatoria es
virtualmente jamás un problema. También, no tiene destructores para la
llamada. ¿Así cuando necesita usted usar a finally en Java?
[3] Un destructor es una función a la que se llama siempre que se deja de
usar un objeto . Siempre se sabe exactamente donde y cuando llamar al
destructor. C++ tiene llamadas automáticas del destructor, y C# (el cual es
bastante parecido a Java) tiene una forma en que la destrucción automática
puede ocurrir.
La cláusula finally es necesaria cuando usted necesita colocar fuera de la
memoria de regreso para su estado original. Esto es alguna clase de limpieza
total como una la conexión del archivo abierto o de la red, algo que usted ha
dibujado en la pantalla, o aun un interruptor en el mundo exterior, tan
modelado en el siguiente ejemplo:
//: c09:Switch.java
public class Switch {
private boolean state = false;
public boolean read() { return state; }
public void on() { state = true; }
public void off() { state = false ; }
} ///:~
//: c09:OnOffException1.java
public class OnOffException1 extends Exception {} ///:~
//: c09:OnOffException2.java
public class OnOffException2 extends Exception {} ///:~
//: c09:OnOffSwitch.java
// Why use finally?
public class OnOffSwitch {
private static Switch sw = new Switch();
public static void f()
throws OnOffException1, OnOffException2 {}
public static void main(String[] args) {
try {
sw.on();
// Codigo que puede lanzar excepciones...
f();
sw.off();
} catch(OnOffException1 e) {
System.err.println( "OnOffException1");
sw.off();
} catch(OnOffException2 e) {
System.err.println( "OnOffException2");
sw.off();
}
}
} ///:~
La meta aquí es asegurarse de que el interruptor está cerrado cuando main()
es completado, así es que sw.off() es colocado al final del bloque try y al final
de cada manipulador de excepción. Pero es posible que una excepción podría
ser lanzada que no es capturado aquí, así es que sw.off() haría falta. Sin
embargo, con finally usted puede colocar el código de limpieza total de un
bloque try en simplemente un lugar:
//: c09:WithFinally.java
// Finally Garantiza limpieza total .
public class WithFinally {
static Switch sw = new Switch();
public static void main(String[] args) {
try {
sw.on();
// Codigo que puede lanzar excepciones...
OnOffSwitch.f();
} catch(OnOffException1 e) {
System.err.println( "OnOffException1");
} catch(OnOffException2 e) {
System.err.println( "OnOffException2");
} finally {
sw.off();
}
}
} ///:~
Aquí el sw.off() ha sido movido a simplemente un lugar, dónde - se asegura corre pase lo que pase.
Aun en los casos en los cuales la excepción no queda atrapada en la corriente
se sedimenta de cláusulas catch , finalmente será ejecutado antes de que el
mecanismo de manipulación de excepción continúe su búsqueda para un
manipulador en el siguiente nivel más alto:
//: c09:AlwaysFinally.java
// Finally es siempre ejecutado.
import com.bruceeckel.simpletest.*;
class FourException extends Exception {}
public class AlwaysFinally {
private static Test monitor = new Test();
public static void main(String[] args) {
System.out.println("Entrando el primer bloque try ");
try {
System.out.println( "Entrando el segundo bloque try");
try {
throw new FourException();
} finally {
System.out.println("finally en el 2do bloque try");
}
} catch(FourException e) {
System.err.println(
"Capturo FourException en el 1er bloque try");
} finally {
System.err.println( "finally en el 1er bloque try");
}
monitor.expect(new String[] {
"Entrando el primer bloque try",
"Entrando el segundo bloque try",
"finally en el 2do bloque try ",
"Capturo FourException en el 1er bloque try ",
"finally en el 1er bloque try "
});
}
} ///:~
La declaración finally también será ejecutada en situaciones en las cuales las
declaraciones break y continue están involucrados. Note que, junto con el
break y continue designado, finally elimina la necesidad para una
declaración del goto en Java.
Peligro: La excepción perdida
En general, la implementación de las excepciones en Java destaca bastante,
pero desgraciadamente tiene un problema. Aunque las excepciones son una
indicación de una crisis en un programa, y nunca debería ignorarse, es posible
que simplemente se pierda una excepción. Esto ocurre con una configuración
particular al hacer uso de la cláusula finally:
//: c09:LostMessage.java
// Como una excepcion puede ser perdida.
// {ThrowsException}
import com.bruceeckel.simpletest.*;
class VeryImportantException extends Exception {
public String toString() {
return "Una excepcion muy importante!";
}
}
class HoHumException extends Exception {
public String toString() {
return "Una excepcion trivial";
}
}
public class LostMessage {
private static Test monitor = new Test();
void f() throws VeryImportantException {
throw new VeryImportantException();
}
void dispose() throws HoHumException {
throw new HoHumException();
}
public static void main(String[] args) throws Exception {
LostMessage lm = new LostMessage();
try {
lm.f();
} finally {
lm.dispose();
}
monitor.expect(new String[] {
"Excepcion en hilo \"main\" Una excepcion trivial ",
"\thasta LostMessage.dispose(LostMessage.java:24)",
"\thasta LostMessage.main(LostMessage.java:31)"
}); }
} ///:~
Se puede ver que no hay evidencia de la VeryImportantException, que
simplemente es reemplazada por la HoHumException en la cláusula finally .
Ésta es una trampa bastante seria, puesto que significa que una excepción
podría perderse completamente, incluso de forma más oculta y difícil de
detectar que en el ejemplo de arriba. Por el contrario, C++ trata la situación
en la que se lanza una segunda excepción antes de que se maneje la primera
como un error de programación fatal. Quizás alguna versión futura de Java
repare este problema (por otro lado, generalmente se envuelve todo método
que lance alguna excepción, tal como dispose() dentro de una cláusula trycatch).
Restricciones a las excepciones
Cuando se sobrescribe un método, sólo se pueden lanzar las excepciones que
se hayan especificado en la versión de la clase base del método. Ésta es una
restricción útil, pues significa que todo código que funcione con la clase base
funcionará automáticamente con cualquier objeto derivado de la clase base (un
concepto fundamental en POO, por supuesto), incluyendo las excepciones.
Este ejemplo demuestra los tipos de restricciones impuestas (en tiempo de
compilación) a las excepciones:
//: c09:StormyInning.java
// Los métodos Sobrescritos que lanzan solo las excepciones
// especificadas en sus versiones de clase base, o
// excepciones derivadas de las excepciones de la clase
// base.
class BaseballException extends Exception {}
class Foul extends BaseballException {}
class Strike extends BaseballException {}
abstract class Inning {
public Inning() throws BaseballException {}
public void event() throws BaseballException {
// Actualmente no tiene para lanzar cualquiera
}
public abstract void atBat() throws Strike, Foul;
public void walk() {} // Atraviesa una excepción no
// comprobada
}
class StormException extends Exception {}
class RainedOut extends StormException {}
class PopFoul extends Foul {}
interface Storm {
public void event() throws RainedOut;
public void rainHard() throws RainedOut;
}
public class StormyInning extends Inning implements Storm {
// OK para agregar nuevas excepciones para constructores ,
// pero usted debe aprobar con las excepciones de la
// clase base:
public StormyInning()
throws RainedOut, BaseballException {}
public StormyInning(String s)
throws Foul, BaseballException {}
// Metodos regular es deben conformar a la clase base:
//! void walk() throws PopFoul {} //Error de Compilacion
// Interfase NO PUEDEN agregar excepciones a metodos
// existentes de la clase base:
//! public void event() throws RainedOut {}
// Si el metodo realmente no existe en la clase base,
// la excepcion esta OK:
public void rainHard() throws RainedOut {}
// Usted puede escoger para no lanzar varias
// excepciones, siempre que si la version base hace:
public void event() {}
// Metodos sobrescritos pueden lanzar excepciones
// heredadas:
public void atBat() throws PopFoul {}
public static void main(String[] args) {
try {
StormyInning si = new StormyInning();
si.atBat();
} catch(PopFoul e) {
System.err.println( "Pop foul" );
} catch(RainedOut e) {
System.err.println( "Rained out");
} catch(BaseballException e) {
System.err.println( "Generic baseball exception" );
}
// Strike no lanza en la version derivada .
try {
// Que ocurre si usted upcast?
Inning i = new StormyInning();
i.atBat();
// Usted puedo capturar las excepciones de la version
// de clase base del metodo:
} catch(Strike e) {
System.err.println( "Strike");
} catch(Foul e) {
System.err.println( "Foul");
} catch(RainedOut e) {
System.err.println( "Rained out");
} catch(BaseballException e) {
System.err.println( "Generic baseball exception" );
}
}
} ///:~
En Inning, se puede ver que tanto el método event() como el constructor
dicen que lanzarán una excepción, pero nunca lo hacen. Esto es legal porque
permite forzar al usuario a capturar cualquier excepción que se pueda añadir a
versiones superpuestas de event(). Como se ve en atBat(), en el caso de
métodos abstractos se mantiene la misma idea.
La interfaz Storm es interesante porque contiene un método (event()) que
está definido en Inning, y un método que no lo está. Ambos métodos lanzan
un nuevo tipo de excepción, RainedOut. Cuando StormyInning hereda de
Inning e implementa Storm, se verá que el método event() de Tormenta no
puede cambiar la interfaz de excepciones de event() en Inning . De nuevo,
esto tiene sentido porque de otra forma nunca se sabría si se está capturando
lo correcto al funcionar con la clase base. Por supuesto, si un método descrito
en una interfaz no está en la clase base, como ocurre con rainHard(),
entonces no hay problema si lanza excepciones.
La restricción sobre las excepciones no se aplica a los constructores. En
StormyInning se puede ver que un constructor puede lanzar lo que desee,
independientemente de lo que lance el constructor de la clase base. Sin
embargo, dado que siempre se llamará de una manera u otra a un constructor
de clase base (aquí se llama automáticamente al constructor por defecto), el
constructor de la clase derivada debe declarar cualquier excepción del
constructor de la clase base en su especificación de excepciones. Fíjese que un
constructor de clase derivada no puede capturar excepciones lanzadas por el
constructor de su clase base.
La razón por la que StormyInning.walk () no compilará es que lanza una
excepción, mientras que Inning.walk () no lo hace. Si se permitiera esto, se
podría escribir código que llamara a Inning.walk() y que no tuviera que
manejar ninguna excepción, pero entonces, al sustituir un objeto de una clase
derivada de Inning se lanzarían excepciones, causando una ruptura del
código. Forzando a los métodos de la clase derivada a ajustarse a las
especificaciones de excepciones de los métodos de la clase base, se mantiene
la posibilidad de sustituir objetos.
El método event() superpuesto muestra que una versión de clase derivada de
un método puede elegir no lanzar excepciones, incluso aunque lo haga la
versión de clase base. De nuevo, esto es genial, puesto que no rompe ningún
código escrito -asumiendo que la versión de clase base lanza excepciones. A
atBat() se le aplica una lógica semejante, pues ésta lanza PopFoul, una
excepción derivada de Foul, lanzada por la versión de clase base de atBat( ).
De esta forma, si alguien escribe código que funciona con Inning y llama a
atBat(), debe capturar la excepción Foul. Dado que PopFoul deriva de Foul,
el manejador de excepciones también capturará PopFoul.
El último punto interesante está en el método main( ). Aquí se puede ver que
si se está tratando con un objeto StormyInning , el compilador te fuerza a
capturar sólo las excepciones específicas a esa clase, pero si se hace una
conversión hacia arriba al tipo base, el compilador te fuerza (correctamente) a
capturar las excepciones del tipo base. Todas estas limitaciones producen un
código de manejo de excepciones más robusto [5].
[5] La Organización Internacional de Normalización C++ agregó restricciones
similares que requieren que excepciones derivados de métodos sea lo mismo
como, o derivadas de, las excepc iones lanzadas por el método de la clase
base. Éste es un caso en el cual C++ puede realmente comprobar
especificaciones de excepción en la fase de compilación.
Es útil darse cuenta de que aunque las especificaciones de excepciones se ven
reforzadas por el compilador durante la herencia, las especificaciones de
excepciones no son parte del tipo de un método, que está formado sólo por el
nombre del método y los tipos de parámetros. Además, justo porque existe
una especificación de excepciones en una versión de clase base de un método,
no tiene por qué existir en la versión de clase derivada del mismo. Esto es
bastante distinto de lo que dictaminan las reglas de herencia, según las cuales
todo método de la clase base debe existir también en la clase derivada. Dicho
de otra forma, "la interfaz de especificación de excepciones" de un método
particular puede estrecharse durante la herencia y superponerse, pero no
puede ancharse - esto es precisamente lo contrario de la regla de la interfaz de
clases durante la he rencia.
Constructores
Cuando se escribe código con excepciones, es particularmente importante que
siempre se pregunte: "Si se da una excepción, ¿será limpiada
adecuadamente?" La mayoría de veces es bastante seguro, pero en los
constructores hay un problema. El constructor pone el objeto en un estado de
partida seguro, pero podría llevar a cabo otra operación -como abrir un
fichero- que no se limpia hasta que el usuario haya acabado con el objeto y
llame a un método de limpieza especial. Si se lanza una excepción desde
dentro de un constructor, puede que estos comportamientos relativos a la
limpieza no se den correctamente. Esto significa que hay que ser
especialmente cuidadoso al escribir constructores.
Dado que se acaba de aprender lo que ocurre con finally, se podría pensar
que es la solución correcta. Pero no es tan simple, puesto que finally ejecuta
siempre el código de limpieza, incluso en las situaciones en las que no se
desea que se ejecute este código de limpieza hasta que acabe el método de
limpie za. Por consiguiente, si se lleva a cabo una limpieza en finally , hay que
establecer algún tipo de indicador cuando el constructor finaliza normalmente,
de forma que si el indicador está activado no se ejecute nada en finally . Dado
que esto no es especialm ente elegante (se está asociando el código de un sitio
a otro), es mejor si se intenta evitar llevar a cabo este tipo de limpieza en el
método finally , a menos que uno se vea forzado a ello.
En el ejemplo siguiente, se crea una clase llamada InputFile que abre un
archivo y permite leer una línea (convertida a String) de una vez. Usa las
clases FileReader y BufferedReader de la biblioteca estándar de E/S de Java
que se verá en el Capítulo 11, pero que son lo suficientemente simples como
para no tener ningún problema en tener su uso básico:
//: c09:Cleanup.java
// Prestando atencion a excepc iones en constructor es.
import com.bruceeckel.simpletest.*;
import java.io.*;
class InputFile {
private BufferedReader in;
public InputFile(String fname) throws Exception {
try {
in = new BufferedReader( new FileReader(fname));
// Otro codigo que podria lanzar excepciones
} catch(FileNotFoundException e) {
System.err.println( "Could not open " + fname);
// No lo abre, asi que no lo cierra
throw e;
} catch(Exception e) {
// Todas las otras excepciones deben cerrarlo
try {
in.close();
} catch(IOException e2) {
System.err.println("in.close() unsuccessful");
}
throw e; // Relanza
} finally {
// No lo cierra aqui!!!
}
}
public String getLine() {
String s;
try {
s = in.readLine();
} catch(IOException e) {
throw new RuntimeException("readLine() failed");
}
return s;
}
public void dispose() {
try {
in.close();
System.out.println( "dispose() successful");
} catch(IOException e2) {
throw new RuntimeException("in.close() failed");
}
}
}
public class Cleanup {
private static Test monitor = new Test();
public static void main(String[] args) {
try {
InputFile in = new InputFile( "Cleanup.java");
String s;
int i = 1;
while((s = in.getLine()) != null)
; // Mejora procesamiento linea por linea aqui…
in.dispose();
} catch(Exception e) {
System.err.println( "Caught Exception in main");
e.printStackTrace();
}
monitor.expect(new String[] {
"dispose() successful"
});
}
} ///:~
El constructor de InputFile toma un parámetro String, que es el nombre del
archivo que se desea abrir. Dentro de un bloque try, crea un FileReader
usando el nombre de archivo. Un FileReader no es particularmente útil hasta
que se usa para crear un BufferedReader con el que nos podemos comunicar
- nótese que uno de los benefic ios de InputFile es que combina estas dos
acciones.
Si
el
constructor
FileReader
no
tiene
éxito,
lanza
una
FileNotFoundException que podría ser capturada de forma separada porque
éste es el caso en el que no se quiere cerrar el archivo, puesto que éste no se
abrió con éxito. Cualquier otra cláusula de captura debe cerrar el archivo,
puesto que fue abierto en el momento en que se entra en la cláusula catch.
(Por supuesto, esto es un truco si FileNotFoundException puede ser lanzado
por más de un método. En este caso, se podría desear romper todo en varios
bloques try .) El método close() podría lanzar una excepción, por lo que es
probado y capturado incluso aunque se encuentra dentro de otro bloque de
otra cláusula catch - es simplemente otro par de llaves para el compilador de
Java. Después de llevar a cabo operaciones locales, se relanza la excepción, lo
cual es apropiado porque este constructor falló, y no se desea llamar al
método asumiendo que se ha creado el objeto de manera adecuada o que sea
válido.
En este ejemplo, que no usa la técnica de los indicadores anteriormente
mencionados, la cláusula finally no es definitivamente el lugar en el que
close() (cerrar) el archivo, puesto que lo cerraría cada vez que se complete el
constructor. Dado que queremos que se abra el fichero durante la vida útil del
objeto InputFile esto no sería apropiado.
El método getLine () devuelve un String que contiene la línea siguiente del
archivo. Llama a readLine(), que puede lanzar una excepción, pero esa
excepción se captura de forma que getLine() no lanza excepciones. Uno de los
aspectos de diseño de las excepciones es la decisión de si hay que manejar
una excepción completamente en este nivel, o hay que manejarla parcialmente
y pasar la misma excepción (u otra), o bien simplemente pasarla. Pasarla,
siempre que sea apropiada, puede definitivamente simplificar la codificación.
En esta situación, el método getLine() convierte la excepción a un
RuntimeException para indicar un error de programación.
Pero por supuesto, el objeto que realiza la llamada es ahora el responsable de
manejar cualquier IOException que pudiera surgir.
El usuario debe llamar al método limpiar() al acabar de usar el objeto
InputFile . Esto liberará los recursos del sistema (como los manejadores de
archivos) que fueron usados por el BufferedReader y/u objetos FileReader
[4]. Esto no se quiere hacer hasta que se acabe con el objeto InputFile, en el
momento en el que se le deje marchar. Se podría pensar en poner esta
funcionalidad en un método finalize(), pero como se mencionó en el Capítulo
4, no se puede estar seguro de que se invoque siempre a finalize() (incluso si
se puede estar seguro de que se invoque, no se sabe cuándo). Éste es uno de
los puntos débiles de Java: toda la limpieza -que no sea la limpieza de
memoria- no se da automáticamente, por lo que hay que informar al
programador cliente de que es responsable, y debe garantizar que se dé la
limpieza usando finalize().
[4] En C++ un destructor se encargaría de esto por ti.
En Cleanup.java se crea un InputFile para abrir el mismo archivo fuente que
crea el programa, se lee el archivo de línea en línea, y se añaden números de
línea. Se capturan de forma genérica todas las excepciones en el método
main(), aunque se podría elegir una granularidad mayor.
Uno de los beneficios de este ejemplo es mostrar por qué se presentan las
excepciones en este punto del libro -no se puede hacer E/S básica sin usar las
excepciones. Las excepciones son tan integrantes de la programación de Java,
especialmente porque el compilador las fortalece, que se puede tener éxito si
no se conoce bien cómo trabajar con ellas.
Emparejamiento de Excepciones
Cuando se lanza una excepción, el sistema de manejo de excepciones busca en
los manejadores más "cercanos" en el mismo orden en que se escribieron.
Cuando hace un emparejamiento, se considera que la excepción ya está
manejada y no se llevan a cabo más búsquedas.
Emparejar una excepción no exige un proceso perfecto entre la excepción y su
manejador. Un objeto de clase derivada se pue de emparejar con un manejador
de su clase base, como se ve en el ejemplo siguiente:
//: c09:Human.java
// Capturando jerarquias de excepcion.
import com.bruceeckel.simpletest.*;
class Annoyance extends Exception {}
class Sneeze extends Annoyance {}
public class Human {
private static Test monitor = new Test();
public static void main(String[] args) {
try {
throw new Sneeze();
} catch(Sneeze s) {
System.err.println( "Caught Sneeze" );
} catch(Annoyance a) {
System.err.println( "Caught Annoyance");
}
monitor.expect(new String[] {
"Caught Sneeze"
});
}
} ///:~
La excepción Sneeze será atrapada por la primera cláusula catch a la que
corresponde, el cua l es el primero, por supuesto. Sin embargo, si usted quita
la primera cláusula catch, saliendo sólo:
try {
throw new Sneeze();
} catch(Annoyance a) {
System.err.println( "Caught Annoyance");
}
El código todavía surtirá efecto porque captura la clase base Sneeze. Ponga de
cualquier otro modo, catch (Annoyance e) capturará a un Annoyance o
cualquier clase derivada de eso. Esto es útil porque si usted decide añadirle
más excepciones derivadas a un método, luego el código cliente del
programador no necesitará cambiar con tal de que el cliente capture las
excepciones de clase base.
Si usted trata de “camuflar” excepciones de clase deriv ada de la clase derivada
poniendo la primera parte de cláusula catch de la clase base, como esto:
try {
throw new Sneeze();
} catch(Annoyance a) {
System.err.println( "Caught Annoyance");
} catch(Sneeze s) {
System.err.println( "Caught Sneeze" );
}
El compilador le dará un mensaje de error, desde que ve que la cláusula catch
de Sneeze nunca puede ser alcanzada.
La alternativa se acerca
Un sistema que manipula excepción es una puerta de trampa que le permite su
programa abandonar ejecución de la secuencia normal de declaraciones. La
puerta de trampa es usada cuando una “condición excepcional” ocurre, tal esa
ejecución normal es ya no posible o deseable. Las excepciones representan
condiciones que el método actual es incapaz de manejar. La razón por la que la
manipulación de excepción que los sistemas fueron desarrollados lo es porque
el acercamiento de ocuparse de cada condición de error posible producida por
cada llamada de función fue demasiada onerosa, y las programadoras
simplemente no la estaban haciendo. Como consecuencia, ignoraban los
errores. Es digno de acechanza que el asunto de conveniencia del programador
en manejar errores fue una motivación de primera para excepciones en primer
lugar.
Una de las líneas directivas importantes en el manipular excepciones es “no
capture una excepción a menos que usted sepa qué hacer con ella.” De hecho,
una de las metas importantes de manipular excepciones es apartar el código
de control de errores del punto donde los errores ocurren. Esto le permite
enfocar la atención en lo que usted quiere lograr en una sección de su código,
y cómo usted va a ocuparse de problemas en una sección separada bien
definida de su código. Como consecuencia, su código de línea principal no es
desordenado con lógica de control de errores, y es mucho más fácil entender y
mantener.
Las excepciones comprobadas complican este panorama un poco, porque le
obligan a añadir c láusulas catch en lugares donde usted no puede estar listo a
manejar un error. Esto resulta en el problema “dañino si es tragado”:
try {
// ... hacer algo util
} catch(ObligatoryException e) {} // Gulp!
Los programadores (yo me incluyo , en la primera edición de este libro)
simplemente harían la cosa más simple, y se tragarían la excepción – a
menudo involuntariamente, pero una vez que usted la hace, el compilador ha
quedado satisfecho, así es que a menos que usted se acuerde de volver a
visitar y corregir el código, la excepción estará perdida. La excepción ocurre,
pero desaparece completamente cuando es tragado. Porque el compilador le
obliga a escribir código de inmediato para manejar la excepción, esto tiene la
apariencia de la solución más fácil si bien es probablemente lo peor que usted
puede hacer.
Horrorizado al darse cuenta de que había hecho esto, en la segunda edición
que “arreglé” el problema imprimiendo el rastro de la pila dentro del
manipulador (como se ve todavía – apropiadamente – en un número de
ejemplos en este capítulo). Mientras esto es útil para rastrear el
comportamiento de excepciones, todavía señala que usted realmente no sabe
qué hacer con la excepción en ese momento en su código. En este pasaje
consideraremos una cierta cantidad de los asuntos y las complicaciones
proviniendo de comprobadas excepciones, y opciones que usted tiene cuándo
está ocupándose de ellos.
Este tema parece simple. Pero no es sólo complicado, que sea también un
asunto de alguna volatilidad. Hay personas que están incondicionalmente
arraigadas en ya sea el aspecto de la cerca y quién considera que la respuesta
correcta (manifiestamente la de ellos) es obvia. Creo que la razón para uno de
estas posiciones es el beneficio bien definido visto en ir de un lenguaje
pobremente escrito como ANSI C para un lenguaje fuerte, (es decir,
comprobada en la fase de compilación) estáticamente escrito como C++ o
Java. Cuando usted hace esa transición (como hice), los beneficios son tan
dramáticos que puede parecer que el comprobar tipos fuertemente estático es
siempre la mejor respuesta para la mayoría de problemas. Mi esperanza es
relatar un poquito de mi evolución, eso ha cuestionado el valor absoluto de
comprobación de tipo fuertemente estática; Claramente, es mero útil
demasiado del tiempo, pero hay una línea borrosa que cruzamos cuando
comienza a meter en la forma y convertirse en un obstáculo (una de mis citas
favoritas es: “Todos los modelos están mal. Algunos tiene utilidad.”).
La Historia
El manipular excepciones se originó en los sistemas como PL/1 y Mesa, y más
tarde apareció en CLU, Smalltalk, Modula -3, Ada, Eiffel, C + +, Python, Java, y
los lenguajes Java Ruby y C #. El diseño Java es similar a C + +, excepto en
lugares donde los diseñadores Java sintieron que el diseño C++ causó
problemas.
Para proveer a programadores de un armazón que más probable usaran para
el control de errores y recuperación, el manipular excepciones fue añadido a
C++ más bien hacia fines del proceso de estandarización, promovido por
Bjarne Stroustrup, el autor original del lenguaje. El modelo para excepciones
C++ provino primordialmente de CLU. Sin embargo, otros idiomas existieron
en aquel entonces que también soportaron manejo de excepción: Ada,
Smalltalk (ambos del cual si tuviesen excepcio nes pero ninguna de las
especificaciones de excepción) y Modula-3 (que incluyó ambas excepciones y
especificaciones).
En su papel seminal [5] en el tema, Liskov y Snyder notifica que un defecto
mayor de lenguajes como C que reporta errores en una moda tra nsitoria está
eso:
“... cada invocación debe ser perseguida por una prueba condicional para
determinar lo que el resultado fue. Este requisito conduce a los programas que
son difíciles para la lectura, y probablemente ineficiente igualmente, así
haciéndole a programadores desistir de hacer señales y manejando
excepciones.”
[5] Barbara Liskov and Alan Snyder: Exception Handling in CLU, IEEE
Transactions on Software Engineering, Vol. SE- 5, No. 6, November 1979. Este
papel no está disponible en la Internet, sólo en forma estampada así es que
usted tendrá que contactar una biblioteca para obtener una copia.
Note que una de las motivaciones originales de manejo de excepción fueron
evitar este requisito, pero con excepciones comprobadas en Java que
comúnmente vemos exactamente esta clase de código. Proceden a decir:
“... requerir que el texto de un manipulador esté apegado a la invocación que
sube la excepción conduciría a los programas ilegibles en los cuales las
expresiones fueron rotas arriba con manipulado res.”
Después del acercamiento CLU al diseñar excepciones C + +, Stroustrup
manifestó que la meta fue reducirse la cantidad de código requerido para
recuperarse de errores. Creo que él observaba que los programadores no
fueron típicamente escribiendo código de control de errores en C porque la
cantidad y colocación de tal código eran atemorizantes y divertidas. Como
consecuencia, estaban acostumbradas a hacerle la forma de la C, ignorar
errores en código y usar depuradores para seguirle la pista a los problemas.
Para usar excepcio nes, estos programadores de C tuvieron que quedar
convencidos para escribir código “adicional” que normalmente no escribían.
Así, para dibujarlos en una mejor forma de manipular errores, la cantid ad de
código que necesitarían “agregar” no debe ser onerosa. Pienso que es
importante recordar esto objetivo al considerar los efectos de excepciones
comprobadas en Java.
C++ trajo una idea adicional más de CLU: La especificación de excepción,
programáticamente indicar en la firma de método lo que las excepciones
pueden resultar de llamar ese método. La especificación de excepción
realmente tiene dos propósitos. Puede decir “origino esta excepción en mi
código, usted la maneja.” Pero también puede tratar de decir “ignoro esta
excepción que puede ocurrir como resultado de mi código, usted lo maneja.”
Hemos estado enfocando en lo “que usted maneja eso ” en parte al mirar a los
mecánicos y sintaxis de excepciones, pero aquí tengo en particular interés en
el hecho de que a menudo ignoramos excepc iones y eso es lo que la
especificación de excepción puede indicar.
En la especificación de excepción C++ no es parte de la información de tipo de
una función. La comprobación sólo de fase de compilación es asegurar que las
especificaciones de excepción son usadas consistentemente; Por ejemplo, si
una función o un método lanza excepciones, entonces las versiones
sobrecargadas o derivadas también deben lanzar esas excepciones. A
diferencia de Java, sin embargo, ninguna comprobación de fase de compilación
ocurre para determinar si o no la función o el método realmente lanzará esa
excepción, o ya sea la especificación de excepción es completa (eso es, ya sea
exactamente describe todas las excepciones que pueden ser lanzadas). Esa
validación ocurre, pero sólo en el tiempo de ejecución. Si una excepción es
lanzada que viola la especificación de excepción, el programa C + + llamará la
función estándar de la bib lioteca unexpected().
Es interesante notar, porque del uso de plantillas, las especificaciones de
excepción no son usadas en absoluto en la biblioteca estándar C++. Las
especificaciones de excepción, luego, pueden tener un impacto significativo en
el diseño de Java Generics (la versión de plantillas C++ de Java, esperado
para aparecer en JDK 1.5).
Perspectivas
Primero, merita anotar que Java eficazmente inventó la excepción comprobada
(claramente inspirado por especificaciones de excepción C++ y el hecho que
programadores C++ típicamente no pierde el tiempo en ellos). Ha sido un
experimento, cuál ningún lenguaje desde entonces ha escogido para
duplicarse.
En segundo lugar, las excepciones comprobadas parecen ser una cosa obvia de
bien estando discernido en ejemplos introductorios y en programas pequeños.
Ha sido sugerido que las dificultades sutiles comienz an a aparecer cuando los
programas comienzan a agrandarse. Por supuesto, la magnitud usualmente no
ocurre de la noche a la mañana; Avanza lentamente. Los lenguajes que no
pueden ser adecuados para proyectos de gran escala sirven para proyectos
pequeños que crecen, y que en algún punto nos damos cuenta de que las
cosas están yendo de manejables a dificultosos. Esto es lo que yo sugiero
puede ser el caso con demasiado tipo inspeccionando; En particular, con
excepciones comprobadas.
La escala del programa parece ser un asunto significativo. Esto es un problema
porque la mayoría de discusiones tienden a usar programas pequeños como
comprobaciones. Uno de los diseñadores C# observó eso:
“El examen de programas pequeños conduce a la conclusión que las
especificaciones requerientes de excepción ambos pudieron realzar
productividad del desarrollador y pudieron realzar calidad de código, pero la
experiencia con proyectos grandes del software sugiere un resultado diferente
– la productividad disminuida y poco o ningún incremento en la calidad de
código.” [6]
[6]
http://discuss.develop.com/archives/wa.exe?A2=ind0011A&L=DOTNET&P=R3
2820
En relación a excepciones libres, los creadores CLU indicaron:
“Sentimos que fue poco realista requerir que el programador provea a o
ls
manipuladores en situaciones donde ninguna acción significativa puede ser
tomada.” [7]
[7] ibid
Al explicar por qué una declaración de función sin especificación quiere decir
que puede lanzar cualquier excepción, en vez de no excepciones, Stroustrup
indica:
“Sin embargo, eso requeriría especificaciones de excepción para esencialmente
cada función, sería una causa significativa para recompilación, e inhibiría
cooperación con software escrito en otros lenguajes. Esto alentaría a los
programadores a corromper los mecanismos que manejan excepción y escribir
código falso para suprimir excepciones. Le proveería un sentido falso de
seguridad a las personas que pasaron por alto la excepción.” [8]
[8] Bjarne Stroustrup, The C++ Programming Language, 3rd editio n, AddisonWesley 1997, pp 376.
Vemos este mismo comportamiento – subvirtiendo las excepciones –
ocurriendo con excepciones comprobadas en Java.
Martin Fowler (el autor de UML Destilado, Refactoring, y los Patrones de
Análisis) me escribió lo siguiente para mí:
“... en general pienso que las excepciones son buenas, pero las excepciones
comprobadas Java son más molesta que ellos merecen la pena.”
Ahora pienso que el paso importante de Java fue unificar el modelo de
información de error, a fin de que todos los errores sean reportados usando
excepciones. Esto no ocurría con C++, porque para resguardar la
compatibilidad con C el modelo viejo de solo ignorar errores estaba todavía
disponible. Pero si ustedes tienen información coherente con excepciones,
entonces las excepciones pueden ser usadas si desearon, y en caso de que no,
se reproducirán fuera para el nivel de más alto (la consola u otro programa del
contenedor). Cuándo Java cambió el modelo C++ a fin de que las excepciones
fueran la única forma para reportar errores, la ejecución adicional de
excepciones comprobadas pudo haberse vuelto menos necesaria.
En el pasado, he sido un creyente fuerte que ambos las excepciones
comprobadas y tipos estáticos fuertes comprobados fueron esenciales para el
desarrollo robusto de programa. Sin embargo, la experiencia anecdótica y
directa [9] con lenguajes que son más dinámicos que estáticos me conduce a
pensar que los grandes beneficios realmente vienen de:
1. Un modelo de reporte de error unificado por excepciones, no obstante si
el programador es forzado el compilador para manipularlos.
2. La comprobación de tipo, a pesar de cuando tiene lugar. Es decir, con tal
de que el uso correcto de un tipo es implementado, no tiene importancia
si ocurre en tiempo de compilación o el tiempo de ejecución.
[9] Indirectamente con Smalltalk por conversaciones con varios
programadores experimentados en este lenguaje; directamente con Python (
www.Python.org ).
Además de esto, hay beneficios de productividad muy significativos para
reduc ir las restricciones de fases de compilación en el programador.
Ciertamente,
la reflexión (y eventualmente, genérico s) está obligada a
compensar para la naturaleza que sobre-constriñe de mecanografía estática
fuerte, como usted verá en el siguiente capít ulo y en un número de ejemplos a
lo largo del libro.
Ya he sido distinguido por una cierta cantidad que lo que digo aquí constituye
blasfemia, y pronunciando estas palabras mi reputación se destruirá, las
civilizaciones caerán, y un porcentaje superior de programar proyectos
errarán. La creencia que el compilador puede salvar su proyecto señalando
errores en la fase de compilación corre fuertemente, pero es importante darse
cuenta de la limitación de qué el compilador puede hacer; En el Capítulo 15,
enfatizo el valor de un proceso automatizado de la constitución y prueba de
unidades, cuál le da lejos más apalancamiento que usted pase inadvertido
tratando de convertir todo en un error de sintaxis. Está que vale en vista de
eso:
Un buen lenguaje de programación es uno que ayuda a los programadores a
escribir buenos programas. Ningún lenguaje de programación les impedirá a
sus usuarios escribir programas malos. [10]
[10] ( Kees Koster, diseñador del lenguaje CDL, citado por Bertrand Meyer,
diseñador del Lenguaje Eiffel). http://www.elj.com/elj/v1/n1/bm/right/.
En cualquier caso, la probabilidad de excepciones comprobadas que están en la
vida alejados de Java parece oscura. Sería demasiado radical de un cambio de
lenguaje, y los proponentes dentro de Sun parecen ser realmente fuertes. Sun
tiene una historia y política de absoluto resguardo de compatibilidad – para
darle un sentido de esto, virtualmente todo software Sun anda en todo
hardware Sun, no importa cuán viejo esté. Sin embargo, si usted se encuentra
co n que algunas excepciones comprobadas recaudan su forma, o
especialmente si usted se encuentra viéndose forzado a percibir excepciones,
sino usted no sabe qué hacer con ellos, aquí están algunas alternativas.
Pasando excepciones a la consola
En los programas simples, como muchos de aquellos en este libro, la forma
más fácil para conservar las excepciones sin escribir una gran cantidad de
código son distribuirlos de main() a la consola. Por ejemplo, si usted quiere
abrir un archivo para leer (algo que usted aprenderá casi en el detalle en el
Capítulo 12), usted debe abrir y debe cerrar a un FileInputStream, lo cual
lanza excepciones. Para un programa simple, usted puede hacer esto (usted
verá este acercamiento usado en numerosos lugares a lo largo de este libro):
//: c09:MainException.java
import java.io.*;
public class MainException {
// Pasa todas las excepciones a la consola:
public static void main(String[] args) throws Exception {
// Abre el archivo:
FileInputStream file =
new FileInputStream("MainException.java");
// Usa el archivo ...
// Cierra el archivo:
file.close();
}
} ///:~
Note que main() es también un método que puede tener una especificación de
excepción, y aquí el tipo de excepción es Exception, la clase raíz de todas las
excepciones comprobadas. Distribuyéndolo a la consola, usted es eximido de
escribir
cláusulas
try-catch
dentro
del
cuerpo
de
main().
(Desafortunadamente, el archivo de Entrada/Salida es significativamente más
complejo que pareciese estar desde este ejemplo, así es que no se ponga
demasiado excitado hasta que usted haya leído el Capítulo 12).
Convirtiendo las excepciones comprobada a no
comprobadas
Lanzar una excepción desde main() es conveniente cuando usted escribe a un
main(), pero no es generalmente útil. El problema real es cuando usted
escribe un cuerpo común de método, y usted llama otro método y se percata
de que “no tengo idea qué hacer con esta excepción aquí, pero no quiero
tragarla o imprimir algún mensaje vulgar.” Con excepciones concatenadas del
JDK 1.4, una solución nueva y simple se evita a sí mismo. Usted simplemente
“envuelve” una excepción comprobada dentro de un RuntimeException, como
éste:
try {
// ... haz algo útil
} catch(IDontKnowWhatToDoWithThisCheckedException e) {
throw new RuntimeException(e);
}
Ésta aparenta ser una solución ideal si usted quiere desactivar la excepción
comprobada – usted no la traga, y usted no tiene que meterlo en la
especificación de excepción de su método, pero por el encadenamiento de
excepción usted no pierde cualquier información de la excepción original.
Esta técnica provee la opción para ignorar la excepción y dejarle a ella
burbujear arriba de la pila de llamada sin estar obligada a escribir cláusulas
try-catch y/o las especificaciones de excepción. Sin embargo, usted todavía
puede capturar y puede manejar la excepción específica usando getCause (),
como visto aquí:
//: c09:TurnOffChecking.java
// "Apagando" excepciones comprobadas.
import com.bruceeckel.simpletest.*;
import java.io.*;
class WrapCheckedException {
void throwRuntimeException(int type) {
try {
switch(type) {
case 0: throw new FileNotFoundException();
case 1: throw new IOException();
case 2: throw new RuntimeException("Where am I?");
default: return;
}
} catch(Exception e) { // Adapta a no comprobado:
throw new RuntimeException(e);
}
}
}
class SomeOtherException extends Exception {}
public class TurnOffChecking {
private static Test monitor = new Test();
public static void main(String[] args) {
WrapCheckedException wce = new WrapCheckedException();
// Usted puede llamar a f() sin un bloque try, y permite a
// RuntimeExceptions salir del metodo:
wce.throwRuntimeException(3);
// O usted puede escoger caputar las excepcion es:
for(int i = 0; i < 4; i++)
try {
if(i < 3)
wce.throwRuntimeException(i);
else
throw new SomeOtherException();
} catch(SomeOtherException e) {
System.out .println("SomeOtherException: " + e);
} catch(RuntimeException re) {
try {
throw re.getCause();
} catch(FileNotFoundException e) {
System.out.println(
"FileNotFoundException: " + e);
} catch(IOException e) {
System.out.println("IOException: " + e);
} catch(Throwable e) {
System.out.println("Throwable: " + e);
}
}
monitor.expect(new String[] {
"FileNotFoundException: " +
"java.io.FileNotFou ndException",
"IOException: java.io.IOException" ,
"Throwable: java.lang.RuntimeException: Donde estoy yo ?",
"SomeOtherException: SomeOtherException"
});
}
} ///:~
WrapCheckedException.throwRuntimeException () contiene código que
genera tipos diferentes de excepciones. Estos son atrapados y enrollados
dentro de objetos RuntimeException, así es que se convierten en la “causa”
de esas excepciones.
En
TurnOffChecking,
usted
puede
ver
que
cabe
llamar
throwRuntimeExcept ion() sin bloque try porque el método no lanza varias
excepciones comprobadas. Sin embargo, cuando usted está listo para capturar
excepciones, usted todavía tiene la capacidad de capturar cualquier excepción
que usted quiere poniendo su código dentro de un bloque try. Usted comienza
por percibir todas las excepciones que usted explícitamente sabe podría
emerger del código en su bloque try – en este caso, SomeOtherException es
atrapado primero. Finalmente, usted captura a RuntimeException y lanza el
resultado de getCause () (la excepción envuelta). Esto extrae las excepciones
originarias, lo cual luego puede ser manipulado en sus cláusulas catch.
La técnica de envolver una excepción comprobada en un RuntimeException
será usada cuando asignará a lo largo del resto de este libro.
Las directivas de excepción
Use excepciones para:
1. Manejar problemas en el nivel apropiado. (Evite capturar excepciones a menos que
se sepa qué hacer con ellas).
2. Arreglar el problema y llamar de nuevo a l método que causó la excepción.
3. Arreglar todo y continuar sin volver a ejecutar el método
4 . Calcular algún resultado alternativo en vez de lo que se suponía que iba a devolver el
método.
5. Hacer lo que se pueda en el contexto actual y relanzar la misma excepción a un
contexto superior.
6. Hacer lo que se pueda en el contexto actual y lanzar una excepción diferente a un
contexto superior.
7 . Terminar el programa.
8. Simplificar. (Si tu esquema de excepción hace algo más complicado, es una molestia
utilizarlo.)
9. Hacer más seguros la biblioteca y el programa. (Ésta es una inversión a corto plazo
de cara a la depuración, y también una inversión a largo plazo de cara a la fortaleza
de la aplicación.)
Resumen
La recuperación de errores mejorada es una de las formas más poderosas para
que usted pueda aumentar la robustez de su código. La recuperación de
errores es una preocupación fundamental para cada programa que usted
escribe, pero es especialmente importante en Java, dónde una de las metas
primarias es crear componentes de programa para otros aprovechar. Para
crear un sistema robusto, cada componente debe ser robusto . Proveyéndole un
consistente modelo de reporte de error con excepciones, Java permite
componentes para confiadamente comunicarle los problemas al código del
cliente.
Las metas para el manejo de excepción en Java son simplificar la creación de
programas grandes, fidedignos usando menos código que actualmente posible,
y para hacer eso con más confianza que su aplicación no tiene un error no
manejado. Las excepciones no son terriblemente difíciles de aprender, y son
una de esas características que le proveen los beneficios inmediatos y
significativos a su proyecto.
Ejercicios
Las soluciones para los ejercicios seleccionados pueden ser encontradas en el
documento electrónico The Thinking en Java Annotated Solution Guide,
disponible para una retribución pequeña de www.BruceEckel.com.
1.
Cree una clase con un main() que lanza un objeto de clase Exception dentro de un
bloque try. Déle al constructor para Exception un argumento String. Capture la
excepción dentro de una cláusula catch e imprima el argumento String. Agregue
una cláusula finally e imprima un mensaje para probar que usted estaba allí.
2.
Cree su clase de excepción usando la palabra clave extends. Escriba un constructor
para esta clase que toma un argumento String y almacena lo dentro del objeto con
una referencia String. Escriba un método que imprime el String almacenado. Cree
una cláusula try-catch para ejercitar su excepción nueva.
3.
Escriba una clase con un método que lanza una excepción del tipo creada en el
Ejercicio 2. Pruebe compilarlo sin una especificación de excepción para ver lo que el
compilador dice. Agregue la especificación apropiada de excepción. Pruebe su clase y
su excepción dentro de una cláusula try -catch .
4.
Defina una referencia del objeto e in icialícela a nulo. Trate de llamar un método a
través de esta referencia. Ahora envuelva el código en una cláusula try-catch para
capturar la excepción.
5.
Cree una clase con dos métodos, f() y g(). En g(), lance una excepción de un tipo
nuevo que usted defin e. En f(), llame a g(), coja su excepción y, en la cláusula catch,
lanza una excepción diferente (de un segundo tipo que usted define). Pruebe su
código en main().
6.
Repita el ejercicio previo, pero dentro de la cláusula catch, envuelva la excepción de
g() en un RuntimeException.
7.
Cree tres tipos nuevos de excepciones. Escriba una clase con un método que lanza
todos los tres. En main (), llame el método sino único usa una única cláusula catch
que atrapará todos los tres tipos de excepciones.
8.
Escriba código para generar y atrapar a un ArrayIndexOutOfBoundsException.
9.
Cree su comportamiento como de reanudación usando un bucle while que repite
hasta que una excepción sea no más lanzada.
10. Cree una jerarquía de tres niveles de excepciones. Ahora cree una clase base A con un
método que lanza una excepción en la base de su jerarquía. Herede B d esde A y
sobrescriba el método conque lanza una excepción en el nivel dos de su jerarquía.
Repita heredando la clase C desde B. En main(), cree una C y dirigido hacia arriba
para A, luego llame el método.
11.
Demuestre que un constructor de clase derivada no puede percibir excepciones
lanzadas por su constructor de clase base.
12. Demuestre que OnOffSwitch.java puede fallar lanzando a un RuntimeException
dentro del bloque try.
13. Demuestre que WithFinally.java no falla lanzando a un RuntimeException
dentro del bloque try.
14. Modifique el Ejercicio 7 agregando una cláusula finally. Compruebe que su cláusula
finally es ejecutada, aun si un NullPointerException es lanzado.
15. Cree un ejemplo donde usted usa una bandera para controlar si el código de limpieza
total es llamado, como descrito en el segundo párrafo después del encabezado
“Constructores.”
16. Modifique a StormyInning.java agregando un tipo de excepción
UmpireArgument y métodos que lanzan esta excepción. Pruebe la jerarquía
modificada.
17. Quite la primera cláusula catch en Human.java y compruebe que el código todavía
compila y corre correctamente.
18. Añádale un nivel de segundo de pérdida de excepción a LostMessage.java a fin de
que el HoHumException es sí mismo reemplazado por una tercera excepción.
19. Añádale un conjunto apropiado de excepciones al c08:GreenhouseControls.java.
20. Añádale un conjunto apropiado de excepciones al c08:Sequence.java.
21. Cambie la cadena de nombre de archivo en MainException.java para nombrar un
archivo que no existe. Corra el programa y note el resultado.
Descargar