java.lang.Throwable
|--Error //内部错误引起,只能通告给用户,并尽力使程序安全地终止
|--Exception //可被处理或抛出的异常
|--RuntimeException //程序错误导致的异常,只能处理,不能throws
|--otherException //程序以外的问题导致的异常,可处理可throws
可以看到,所有Error
和Exception
拥有共同的父类——Throwable
,用户自定义的异常类也必须继承自Throwable
,只有Throwable
及其子类才可以用throw/throws
关键字抛出。
Java语言规范将派生于Error
和RuntimeException
类的异常称为非受查(unchecked)异常,其他所有异常被称为受查(checked)异常。区别在于,对于非受查异常,Java编译器不要求捕获或一定要抛出,但必须要捕获或抛出受查异常。
public class Test {
public static void main(String[] args) {
Test p = new Test();
p.done();
}
public void done() throws ArithmeticException{
System.out.println();
}
}
如,done()
方法声明抛出的是ArithmeticException extends RuntimeException
,属于非受查异常,所以main()
函数可以不去处理。
public FileInputStream(String name)throws FileNotFoundException{
...
}
如上,throws
声明表示FileInputStream
这个构造器可能会抛出一个FileNotFoundException
类对象。
1)调用一个抛出异常的方法,如FileInputStream
构造器
2)程序中发生错误,并且利用throw
语句抛出一个受查异常
3)程序出错,如,a[-1] = 0
会抛出ArrayIndexOutOfBoundsException
非受查异常
4)Java虚拟机和运行时库出现的内部错误
其中,1)2)必须在首部声明这些方法可能抛出异常,3)指的是从RuntimeException
继承的异常,应该尽量避免此类异常发生,而不是说明这些异常发生的可能性,4)其实就是Error
,我们对其没有任何控制能力。
所以,一个方法必须声明所有可能抛出的受查异常,而非受查异常要么不可控Error
,要么应该避免RuntimeException
,不需要声明异常发生的可能性。
public Image loadImage(String s)throws FileNotFoundException, EOFException //抛多个异常
{
...
}
如果子类覆盖了超类中的一个方法,子类方法中声明的受查异常不能比超类方法中声明的异常更通用,即,如果超类方法抛出IOExceptipn
,那么子类方法只能抛出IOException
的子类异常,或者根本不抛任何异常。
String readData(Scanner in)throws EOFException
{
...
while(...)
{
if(!in.hasNext()) //EOF encountered
{
if(n < len)
throw new EOFException;
}
...
}
return s;
}
1)选择合适的异常类或创建一个异常类
2)创建该类的一个对象
3)抛出
throw
语句执行后,其后的语句不再执行,finally
中的语句除外。
try/catch/finally
语句块来捕获异常
/*完整形式*/
try
{
code
more code
}
catch(ExceptionType e)
{
handler for this type
}
finally
{
code that must be executed
}
/*一般形式*/
try
{
...
}
catch(ExceptionTyepe e) //可以有多个catch捕获不同的异常
{
...
}
/*其他形式*/
try
{
...
}
finally
{
...
}
try
语句块中方法的任何代码抛出的异常类型必须在catch
子句中声明了,程序才会跳转到对应catch
子句,如果抛出的异常类型没有声明的话,那么这个方法就会立刻退出。
捕获或抛出异常需要谨慎选择,如果是知道如何处理的异常,就应该捕获,而那些不知道如何处理的异常则应该继续向外抛出去。
有一种例外的情况,即编写一个覆盖超类的方法,而被覆盖的超类方法没有抛出异常,那么子类方法就必须捕获方法代码中可能出现的每一个受查异常,而不能throws
抛出。
Java SE 7以后,同一个catch
子句中可以捕获多个异常类型。
try
{
code that might throw exception
}
catch(FileNotFoundException | UnknownHostException e)
{
emergency action for missing files and unknown hosts
}
如上,FileNotFoundExcepiton
和UnknownHostException
两个异常捕获子句合并为一个,即发生两个之中任意一个异常时,都会执行子句体中的内容。
但是,捕获多个异常时,异常变量e
隐含为final
变量,不能再为e
赋不同的值。
在catch
子句中throw
一个异常,这样做可以改变异常的类型,实现异常转换的效果。一般用于封装某一模块时,不想直接提供具体错误的细节原因,而是用一个更抽象的异常来描述错误信息,如ServletException
。
try
{
access the database
}
catch(SQLException e)
{
throw new ServletException("database error:" + e.getMessage());
}
在上述例子中,向外抛出具体的异常,如SQLException
,过于细节了,这是调用者不关注的,调用者关注的只是从Servlet
层面上发生了什么问题,所以我们可以通过再次抛出异常将具体异常转换成带有异常信息文本的超类异常,即ServletException
。
try
{
access the database
}
catch(SQLException e)
{
Throwable se = new ServletException("database error");
se.initCause(e);
throw se;
}
更优的处理方法如上,不是直接抛出原异常的超类异常,而是将原始异常设置为新异常(超类异常)的cause
,这样在捕获到异常的时候,仍然可以通过处理重新得到原始异常。
Throwable e = se.getCause();
这是一种包装技术,既能使抛出的异常不过于局限在某一细节上,又不会丢失原始异常的细节,随时可以回溯。
finally
子句的特点是,无论是否有异常被捕获,子句中的代码都会被执行,常被用来执行资源回收相关的语句。
一般的用法是,try/catch/finally
连用,try/catch
捕获异常,finally
关闭资源,但是这种情况会造成强耦合,所以一般建议分开处理,提高代码清晰度:
InputStream in = ...;
try
{
try
{
code that might throw exception
}
finally
{
in.close();
}
}
catch(IOException e)
{
show error message
}
这样解耦合处理,使内层的try/finally
语句块只用来确保关闭输入流,而外层的try/catch
语句块只用来确保捕获报告中出现的错误。这种设计思路清晰,同时,还能报告finally
子句中可能出现的错误。
public static int f(int n)
{
try
{
int r = n * n;
return r; //执行此句之前,会先执行finally子句中的内容
}
finally
{
if(n == 2)
return 0; //如果执行到此句,将会直接返回值,结束f()函数,不再跳回try子句中执行return语句
}
}
所以,会出现f(3) = 9
而f(2) = 0
,两种不同的返回情况。
针对实现了AutoCloseable
或Closeable
接口的资源类,接口中规定了对应的close()
方法:
public interface AutoCloaseable
{
public void close() throws Exception;
...
}
public interface Closeable
{
public void close throws IOException;
...
}
在使用实现了以上两种接口的资源类时,可以使用带资源的try
语句
try(Resource res1 = ...; Resource res2 = ...; ...)
{
work with res1
work with res2
...
}
try
块正常退出时,或存在一个异常时,都会自动调用res.close()
方法,等同于使用了finally
块。
普通的try/finally
语句中,如果try
和finally
块抛出了同样的异常,那么finally
中的异常会覆盖掉try
中的异常,而带资源的try
语句可以很好的避免这个问题。
在带资源的try
语句中,发生上述情况时,原来的异常(try
块中的异常)会重新抛出,而close()
方法(finally
块中)抛出的异常会“被抑制”。这些异常将被自动捕获,并由addSuppressed
方法添加到原来的异常,如果对这些异常感兴趣,可以调用getSuppressed
方法得到从close
方法抛出并被抑制的异常列表。
如尝试上百万次地对一个空栈执行退栈操作,在实施退栈操作之前,首先查看栈是否为空。有两种写法:
//第一种
if(!s.empty())
{
s.pop();
}
//第二种
try
{
s.pop();
}
catch(EmptyStackException e)
{
}
结果是,调用isEmpty
的版本的运行时间为646毫秒,捕获EmptyStackException
的版本运行时间为21739毫秒,采用异常机制花费的时间大大超过了常规写法,所以使用异常的基本规则是:只有在异常情况下使用异常机制。
如
PrintStream out;
Stack s;
for(int cnt = 0; cnt < 100; cnt ++)
{
try
{
n = s.pop();
}
catch(EmptyStackException e)
{
//stack was enpty
}
try
{
out.writeInt(n);
}
catch(IOException e)
{
//problem writing to file
}
}
这种编程方式将导致代码量的急剧膨胀,而且执行速度比较慢。所以,有必要把整个任务包装在一个try
语句块中,这样当任何一个操作出现问题时,整个任务都可以取消。
try
{
for(int cnt = 0; cnt < 100; cnt++)
{
n = s.pop();
out.writeInt(n);
}
}
catch(IOException e)
{
//problem writing to file
}
catch(EmptyStackException e)
{
//stack was empty
}
不要只抛出RuntimeException
异常,应该寻找更加适当的子类或创建自己的异常类。
不要只捕获Throwable
异常,否则,会使程序代码晦涩难懂。
适当地将一种异常转换成另一种更加适合的异常。如,在解析某个文件中的一个整数时,捕获NumberFormatException
异常,然后将它转换成IOException
或MySubsystemException
。
举个例子,当栈空时,Stack.pop
是返回一个null
,还是抛出一个异常?
通常,我们认为,在出错的地方抛出一个EmptyStackException
异常要比在后面抛一个NullPointException
异常更可取。
并不是要尽可能多得捕获异常。如果调用了一个抛出异常的方法,例如,FileInputStream
构造器或readLine
方法,这些方法就会本能地捕获这些可能产生的异常。其实,传递异常要比捕获这些异常更好:
public void readStuff(String filename) throws IOException //not a sign of shame
{
InputStream in = new FileInputStream(filename);
...
}
让更高层次的方法通知用户发生了错误,或者放弃不成功的命令更加合适。