Java核心技术 卷1-总结-8
处 理 错 误
如何抛出异常
一个名为readData
的方法正在读取一个首部具有下列信息的文件:
Content-length:1024
然而,读到733个字符之后文件就结束了。这是一种不正常的情况,希望抛出一个异常。
首先要决定应该抛出什么类型的异常。将上述异常归结为IOException
是一种很好的选择。仔细地阅读Java API文档之后会发现:EOFException
异常正是我们要抛出的异常。下面是抛出这个异常的语句:
throw new EOFException(); //或者
EOFException e = new EOFException();throw e;
下面将这些代码放在一起:
String readData(Scanner in) throws EOFException {while(...) {if (!in.hasNext()) // EOF encountered {if (n < len) {throw new EOFException();} }}return s;
}
对于一个已经存在的异常类,将其抛出非常容易。在这种情况下:
1. 找到一个合适的异常类。
2. 创建这个类的一个对象。
3. 将对象抛出。
一旦方法抛出了异常,这个方法就不可能返回到调用者。也就是说,不必为返回的默认值或错误代码担忧。
创建异常类
在程序中,可能会遇到任何标准异常类都没有能够充分地描述清楚的问题。在这种情况下,需要创建自己的异常类。需要做的只是定义一个派生于Exception
的类,或者派生于Exception
子类的类。 例如,定义一个派生于IOException
的类。习惯上,定义的类应该包含两个构造器,一个是默认的构造器;另一个是带有详细描述信息的构造器(超类Throwable
的toString
方法将会打印出这些详细信息,这在调试中非常有用)。
class FileFormatException extends IOException {public FileFormatException() {}public FileFormatException(String gripe) { super(gripe);}
}
这样就可以抛出自己定义的异常类型了。
String readData(BufferedReader in) throws FileFormatException {while(...) {if (ch ==-1) { // EOF encounteredif (n < len) {throw new FileFormatException();}}}return s;
}
捕获异常
抛出一个异常十分容易。只要将其抛出就不用理睬了。但是,有些代码必须捕获异常。捕获异常需要进行周密的计划。
捕获异常
如果某个异常发生的时候没有在任何地方进行捕获,那程序就会终止执行,并在控制台上打印出异常信息,其中包括异常的类型和堆栈的内容。
要想捕获一个异常,必须设置try/catch语句块。最简单的try语句块如下所示:
try { code
}
catch (ExceptionType e) {handler for this type
}
如果在try语句块中的任何代码抛出了一个在catch子句中说明的异常类,那么
1. 程序将跳过try语句块的其余代码。
2. 程序将执行catch子句中的处理器代码。
如果在try语句块中的代码没有抛出任何异常,那么程序将跳过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();}
}
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说明符,以便告知调用者这个方法可能会抛出异常。
如果编写一个覆盖超类的方法,而这个方法又没有抛出异常,那么这个方法就必须捕获方法代码中出现的每一个受查异常。不允许在子类的throws说明符中出现超过超类方法所列出的异常类范围。
捕获多个异常
在一个try语句块中可以捕获多个异常类型,并对不同类型的异常做出不同的处理。可以按照下列方式为每个异常类型使用一个单独的catch子句:
try {code that might throw exceptions
}
catch(FileNotFoundException e) {emergency action for missing files
}
catch (UnknownHostException e) {emergency action for unknown hosts
}
catch (IOException e) {emergency action for all other I/O problems
}
异常对象可能包含与异常本身有关的信息。要想获得对象的更多信息,可以试着使用e.getMessage()
得到详细的错误信息(如果有的话),或者使用e.getClass().getName()得到异常对象的实际类型。
再次抛出异常与异常链
在catch子句中可以抛出一个异常,这样做的目的是改变异常的类型。 如果开发了一个供其他程序员使用的子系统,那么,用于表示子系统故障的异常类型可能会产生多种解释。ServletException就是这样一个异常类型的例子。执行servlet的代码可能不想知道发生错误的细节原因,但希望明确地知道servlet是否有问题。下面给出了捕获异常并将它再次抛出的基本方法:
try {access the database
}
catch (SQLException e) {throw new ServletException("database error:" + e.getMessage());
}
这里,ServleException用带有异常信息文本的构造器来构造。
有时你可能只想记录一个异常,再将它重新抛出,而不做任何改变:
try {access the database
}
catch (Exception e) {logger.log(level,message,e);throw e;
}
finally子句
当代码抛出一个异常时,就会终止方法中剩余代码的处理,并退出这个方法的执行。 如果方法获得了一些本地资源,并且只有这个方法自己知道,又如果这些资源在退出方法之前必须被回收,那么就会产生资源回收问题。Java中的finally子句可以很好的解决这些问题。不管是否有异常被捕获,finally子句中的代码都被执行。 在下面的示例中,程序将在所有情况下都会关闭文件。
InputStream in = new FileInputStream(....);
try {//1code that might throw exceptions //2
}
catch (IOException e) {//3show error message //4
}
finally {//5 in.close();
}
//6
在上面这段代码中,有下列4种情况会执行finally子句:
- 代码没有抛出异常。在这种情况下,程序首先执行try语句块中的全部代码,然后执行finally子句中的代码。随后,继续执行try语句块之后的第一条语句。也就是说,执行标注的1、2、5、6处。
- 抛出一个在catch子句中捕获的异常。在这种情况下,程序将执行try语句块中的所有代码,直到发生异常为止。此时,将跳过try语句块中的剩余代码,转去执行与该异常匹配的catch子句中的代码,最后执行finally子句中的代码。
- 如果catch子句没有抛出异常,程序将执行try语句块之后的第一条语句。在这里,执行标注1、3、4、5、6处的语句。
- 代码抛出了一个异常,但这个异常不是由catch子句捕获的。在这种情况下,程序将执行try语句块中的所有语句,直到有异常被抛出为止。此时,将跳过try语句块中的剩余代码,然后执行finally子句中的语句,并将异常抛给这个方法的调用者。在这里,执行标注1、5处的语句。
try 语句可以只有finally子句,而没有 catch子句。 例如,下面这条try 语句:
InputStream in = ...;
try {code that might throw exceptions
}
finally { in.close();
}
无论在try语句块中是否遇到异常,finally子句中的in.close()语句都会被执行。
当finally子句包含return语句时,将会出现一种意想不到的结果。假设利用return语句从try语句块中退出。在方法返回前,finally子句的内容将被执行。如果 finally子句中也有一个return语句,这个返回值将会覆盖原始的返回值。 请看一个复杂的例子:
public static int f(int n) {try {intr = n * n;return r;} finally { if (n == 2) {return 0;}}
}
如果调用f(2)
,那么try语句块的计算结果为r=4
,并执行return
语句。然而,在方法真正返回前,还要执行finally子
句。finally
子句将使得方法返回0
,这个返回值覆盖了原始的返回值4
。
有时候,finally
子句也会带来麻烦。例如,清理资源的方法也有可能抛出异常。在执行finally语句块,并调用close方法时。close方法本身也有可能抛出IOException
异常。当出现这种情况时,原始的异常将会丢失,转而抛出close方法的异常。 这会有问题,因为第一个异常很可能更重要。如果想重新抛出原来的异常,代码会变得极其繁琐。如下所示:
InputStream in =...;
Exception ex = null;
try {try {code that might throw exceptions }catch (Exception e) { ex = e;throw e; }
}
finally { try {in.close();}catch (Exception e) {if (ex == null) { throw e;}}
}
带资源的try语句可以很好的解决这个问题。
带资源的try 语句
对于以下代码模式:
open a resource
try {work with the resource }
finally {close the resource
}
假设资源属于一个实现了AutoCloseable
接口的类,Java SE7及以上为这种代码模式提供了一个很有用的快捷方式。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("/usr/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
方法抛出的异常会“被抑制”。只要需要关闭资源,就要尽可能使用带资源的try 语句。
注:带资源的try
语句自身也可以有catch
子句和一个finally
子句。这些子句会在关闭资源之后执行。