1.捕获异常
如果某个异常发生的时候没有在任何地方进行捕获,那程序就会终止执行,并在控制台上打印出异常信息,其中包括异常的类型和堆栈的内容。对于图形界面程序(applet和应用程序),在捕获异常之后,也会打印出堆栈的信息,但程序将返回到用户界面的处理循环中。
要想捕获一个异常,必须设置try/catch语句块。最简单的try语句块如下所示:
try
{
code
more code
more code
}
catch (ExceptionType e)
{
handle for this type
}
如果在try语句块中的任何代码抛出了一个在catch子语句中说明的异常类,那么
1)程序将跳过try语句的其余代码。
2)程序将执行catch子句中的处理器代码
如果在try语句中的代码没有抛出任何异常,那么程序将跳过catch子句。
如果方法中的任何代码抛出了一个在catch子句中没有声明的异常类型,那么这个方法就会立刻退出(希望调用者为这种类型的异常设计了catch子句)。
例:
public void read(String filename)
{
try
{
InputStream in=new FileInputStream(filename);
int b;
while((b=in.read())!=-1)
{
process input
}
}
catch (IOException exception)
{
exception.printStackTrace();
}
}
try语句中的大多数代码都很容易理解:读取并处理字节,直到遇到结束符位置,read方法可能抛出一个IOException异常。在这种情况下,将跳出整个while循环,进入catch子句,生成一个栈轨迹。还有其他选择吗?
通常最好的选择是什么也不做,而是将异常传递给调用者。如果read方法出现了错误,就让read方法的调用者去操心!如果采用这种方法,就必须声明可能会抛出一个IOException。
public void read(String filename) throws IOException
{
InputStream in=new FileInputStream(filename);
int b;
while((b=in.read())!=-1)
{
process input
}
}
通常应该捕获那些知道如何处理的异常,而将那些不知道怎么样处理的异常继续传递。如果传递一个异常,就必须在方法的首部添加一个throws说明符,以便告知调用者这个方法可能会抛出异常。
仔细阅读以下Java API文档,以便知道每个方法可能会抛出哪种异常,然后再决定是自己处理,还是添加到throws列表中。
如果编写一个覆盖超类的方法,而这个方法又没有抛出异常,那么这个方法就必须捕获方法代码中出现的每一个受查异常。不允许在子类的throws说明符中出现过超类方法所列出的异常类范围。
2.捕获多个异常
在一个try语句块中可以捕获多个异常类型,并对不同类型的异常做出不同的处理。可以按照下列方式为每个异常类型使用一个单独的catch子句:
try
{
code that might throw exceptions
}
catch (FileNotFoundException e)
{
emergency action for missing files
}
catch (UncknowHostException e)
{
emergency action for unknown hosts
}
catch (IOExcception e)
{
emergency action for all other I/O problems
}
异常对象可能包含与异常本身有关的信息。要想获得对象的更多信息,可以试着使用
e.getMessage()
在Java SE 7中,同一个catch子句中可以捕获多个异常类型。例如,假设对应缺少文件和未知主机异常的动作是一样的,就可以合并catch子句:
try
{
code that might throw exceptions
}
catch (FileNotFoundException | UnknowHostException e)
{
emergency action for missing files and unknown hosts
}
catch(IOException e)
{
emergency action for all other I/O problems
}
只有当捕获的异常类型彼此之间不存在子类关系时才需要这个特性。
3.再次抛出异常与异常链
在catch子句中可以抛出一个异常,这样做的目的是改变异常的类型。如果开发了一个供其他程序员使用的子系统,那么,用于表示子系统故障的异常类型可能会产生多种解释。
ServeletException就是这样一个异常类型的例子。执行servlet的代码可能不想知道发生错误的细节原因,但希望明确地知道servlet是否有问题。
下面给出捕获异常并将它再次抛出的基本方法:
try
{
access the database
}
catch(SQLException e)
{
throw new ServletException("database error:"+e.getMessage());
}
这里,ServeleException用带有异常信息文本的构造器来构造。不过更好的是将原始异常设置为新异常的“原因”:
try
{
access the database
}
catch(SQLException e)
{
Throwable se=new ServletException("database error");
se.initCause(e);
throw se;
}
当捕获到异常时,就可以使用下面这条语句重新得到原始异常:
Throwable e=se.getCause();
有时候你可能只想记录一个异常,再将它重新抛出,而不做任何改变:
try
{
access the database
}
catch(Exception e)
{
logger.log(level,message,e);
throw e;
}
4. finally子句
当代码抛出一个异常时,就会终止方法中剩余代码的处理,并退出这个方法的执行。如果方法获得了一些本地资源,并且只有这个方法自己知道,如果这些资源在退出方法之前必须被回收,那么就会产生资源回收问题。
Java中有解决方案,就是finally子句。如果使用Java编写数据库程序,就需要使用同样的技术关闭与数据库的连接。当发生异常时,恰当地关闭所有数据库的连接是非常重要的。
不管是否有异常被捕获,finally子句中的代码都会被执行。这个例子,程序将在所有情况下关闭文件:
InputStream in=new FileInputStream(......);
try
{
//1
code that might throw exceptions
//2
}
catch(IOException e)
{
//3
show error message
//4
}
finally
{
//5
in.close();
}
//6
上面这段代码中,三种情况会执行finally子句:
- 代码没有异常。这种情况下,程序首先执行try语句块中的全部代码,然后执行finally子句中的代码。随后继续执行try语句之后的第一条语句。也就是1->2->5->6
- 抛出一个在catch子句中捕获的异常。上例中是IOException异常。这种情况下,程序将执行try语句开中所有代码,直到发生异常。此时将跳过try语句块中的剩余代码,转去执行与该异常匹配的catch子句中的代码,最后执行finally子句中的代码。1->3->4->5->6
- 代码抛出了一个异常,但这个异常不是由catch子句捕获的。这种情况下,程序将执行try语句块中的所有语句,直到有异常被抛出为止。此时将跳过try语句块中的剩余代码,然后执行finally子句中的语句,并将异常抛给这个方法的调用者。
try语句可以只有finally语句,而没有catch子句。
InputStream in=.....;
try
{
code that might throw exceptions
}
finally
{
in.close();
}
无论在try语句块是否遇到异常,finally子句中的in.close()语句都会被执行。当然如果真的遇到一个异常,这个异常将会被重新抛出,并且必须由另一个catch子句捕获。
在这里,强烈建议解耦合try/catch和try/finally语句块,这样可以提高代码的清晰度。
InputStream in=......;
try
{
try
{
code that might throw exceptions
}
finally
{
in.close();
}
}
catch(IOException e)
{
show error message
}
内层的try语句块只有一个职责,就是确保关闭输入流。外层的try语句块也只有一个职责,就是确保报告出现的错误。
有时候,finally子句也会带来麻烦。例如,清理资源的方法也有可能抛出异常。假设希望能够确保在流处理代码中遇到异常时将流关闭。
InputStram in=.....;
try
{
code thart might throw exceptions
}
finally
{
in.close();
}
5.带资源的try语句
对于以下代码模式:
open a resource
try
{
work with the resource
}
finally
{
close the resouce
}
假设资源属于实现了AutoCloseable接口的类,AutoCloseable接口有一个方法:
void close() throws Exception
带资源的try语句(try-with-resources)的最简形式为:
try(Resource res=...)
{
work with res
}
try块退出时,会自动调用res.close()。下面例子读取一个文件中的所有单词:
try (Scanner in=new Scanner(new FileInputStream("/usr/share/dict/words")), "UTF-8")
{
while(in.hasNext())
System.out.println(in.next());
}
这个块正常退出或者存在一个异常时,都会调用in.close()方法,就好像使用了finally块一样。
还可以指定多个资源:
try(Scanner in=new Scanner(new FileInputStream("/use/share/dict/words/"),"UTF-8");
PrintWriter out=new PrintWriter("out.txt")
{
while(in.hasNext())
out.println(in.next().toUpperCase());
}
不论这个块如何退出,in和out都会关闭。如果用常规方式手动编程,就需要两个嵌套的try/finally语句。
如果try块抛出一个异常,而且close方法也抛出一个异常,这就会带来一个难题。带资源的try语句可以很好地处理这个问题。原来的异常会重新抛出,而close方法抛出的异常会“被抑制”。这些异常将自动补货,并由addSuppressed方法增加到原来的异常。
只要需要关闭资源,就要尽可能使用带资源的try语句。
6.分析堆栈轨迹元素
堆栈轨迹(stack trace)是一个方法调用过程的列表,它包含了程序执行过程中方法调用的特定位置。当Java程序正常终止,而没有捕获异常时,这个列表就会显示。
使用getStackTrace方法,可以得到StackTraceElement对象的一个数组,可以在你的程序中分析这个对象数组,如:
Throwable t=new Throwable();
StackTraceElement[] frames=t.getStackTrace();
for(StackTraceElement frame:frames)
analyze frame
StackTraceElement类含有能够获得文件名和当前执行的代码号的方法,同时,还含有能够获得类名和方法名的方法。toString方法将产生一个格式化的字符串,其中包含所获得的信息,
静态的Thread.getAllStackTrace方法,可以产生所有线程的堆栈轨迹:
Map<Thread,StackTraceElement[]> map=Thread.getAllStackTrace();
for(Thread t:map.keySet())
{
StackTraceElement[] frames=map.get(t);
analyze frames
}
例子:打印递归阶乘函数的堆栈情况。计算factorial(3),将会打印下列内容:
代码:
API java.lang.Throwable 1.0
Throwable(Throwable cause) 1.4
Throwable(String message,Throwable cause) 1.4
//用给定的“原因”构造一个Throwable对象
Throwable initCause(Throwable cause) 1.4
//将这个对象设置为“原因”。如果这个对象已经被设置为“原因”,则抛出一个异常。返回this引用
Throwable getCause() 1.4
//获得设置为这个对象的“原因”的异常对象。如果没有设置“原因”,则返回null
StackTraceElement[] getStackTrace() 1.4
//获得构造这个对象时调用堆栈的跟踪
void addSuppressed(Throwable t) 7
//为这个异常增加一个“抑制”异常”。这出现在带资源的try语句中。其中t是close方法抛出的一个异常
Throwable[] getSuppressed() 7
//得到这个异常的所有“抑制”异常。一般是带资源的try语句中的close方法抛出的异常。
API java.lang.Exception 1.0
Exception(Throwable cause) 1.4
Exception(String message,Throwable cause)
//用给定的“原因”创造一个异常对象
API java.lang.RuntimeException 1.0
RuntimeException(Throwable cause) 1.4
RuntimeException(String message,Throwable cause) 1.4
//用给定的“原因”构造一个RuntimeException对象
API java.lang.StackTraceElement 1.4
String getFileName()
返回这个元素运行时对应的源文件名。如果这个信息不存在,返回null
int getLineNumber()
返回这个元素运行时对应的源文件行数。如果这个信息不存在,返回-1
String getClassName()
返回这个元素运行时对应的类的完全限定名
String getMethodName()
返回这个元素运行时对应的方法名。构造器名是<init>;静态初始化器名是<clinit>。这里无法区分同名的重载方法。
boolean isNativeMethod()
如果这个元素运行时在一个本地方法中,则返回true
String toString()
如果存在的话,返回一个包含类名,方法名,文件名和行数的格式化字符串