捕获异常 - try/catch

2019-08-14  本文已影响0人  hellokitty小丸子

一、异常处理

从上篇文章已经知道如何抛出一个异常,其实除了声明异常之外,还可以捕获异常。那异常出现后到底该如何处理呢?如何决定一个异常是被捕获,还是被抛出让其他的处理器进行处理?
(1)第一种很简单,抛出就不用再管,让异常处理器做处理。异常处理的任务就是将控制权从错误产生的地方转移给能够处理这种情况的异常处理器。方法抛出异常后,调用这个方法的代码也将无法继续执行,取而代之的是,异常处理机制开始搜索能够处理这种异常状况的异常处理器并处理。
(2)捕获异常,这样会使异常不被跑到方法之外,也不需要throws规范。另外,有些代码必须捕获异常。本文重点说捕获异常:

异常处理方式.png
那么两种处理方式分别可以处理什么样的异常呢?
异常处理.png

二、捕获异常

要想捕获一个异常,必须设置try/catch语句块。

try
{
    code
    more code
}
catch(ExceptionType e)
{
    handler for this type
}

如果在try语句块中的任何代码抛出了一个在catch子句中说明的异常类,那么:
(1)程序将跳过try语句块的其他代码。
(2)程序将执行catch子句中的处理器代码。
如果在try语句块中的代码没有抛出任何异常,那么程序将跳过catch子句。如果方法中的任何代码抛出来一个在catch子句中没有声明的异常类型,那么这个方法就会立刻退出,这就希望方法的调用者为这种类型的异常设计catch子句。

三、哪种方法更好呢?

以下演示2中异常处理的过程:
try/catch:这种处理很合乎情理。

public void read(String filename)
{
    try
    {
        code
        more code
     }
    catch(IOException exception)
    {
        exception.printStackTrace();
    }
}

方法首部声明:这种方法是方法编写者什么也不做,而是将异常传递给调用者。

public void read(String filename)  throws IOException
{
    code
    more code
}

哪种方法更好呢?通常,应该捕获那些知道如何处理的异常,而将那些不知道怎样处理的异常继续进行传递。如果想传递一个异常,就必须在方法的首部添加一个throws说明符,以便告知调用者这个方法可能会抛出异常。
可以通过阅读Java API文档知道每个方法可能会抛出的异常,然后决定是自己处理还是添加到throws列表中。对于后一种情况,也不必犹豫,将异常直接交给能胜任的处理器进行处理要比压制对它的处理更好。
需要注意的是:这个规则也有一个例外。上篇文章曾提到过:如果编写一个覆盖超类的方法,而这个方法又没有抛出异常,那这个方法就必须捕获出现的每一个受查异常,不允许在子类的throws说明符中出现超过超类所列出的异常类范围。

二、捕获多个异常

在一个try/catch语句块中可以捕获多个异常类型,并对不同类型的异常做出不同的处理,每个异常类型使用一个单独的catch子句:

try
{
    code
    more code
}
catch(FileNotFoundException e)
{
    missing files
}
catch(UnknownHostException e)
{
    unknown hosts
}
catch(IOException e)
{
    all other I/O problems
}

异常对象可以包含与异常本身有关的信息。要想获得对象的更多信息,可以使用e.getMessage()得到详细的错误信息(如果有),或者使用e.getClass().getName得到异常对象的实际类型。
同一个catch子句中可以捕获多个异常类型。如:假设对应FileNotFoundException和UnknownHostException异常的动作是一样的,就可以合并catch子句:

try
{
    code
    more code
}
catch(FileNotFoundException |  UnknownHostException e)
{
    missing files and unknown hosts
}
catch(IOException e)
{
    all other I/O problems
}

只有当捕获的异常类型彼此之间不存在子类关系时才需要这个特性。如:FileNotFoundException | IOException就不合理了,因为FileNotFoundException是IOException的子类。另外需要注意的是,捕获多个异常时,异常变量隐含为final变量,不能再为e赋值(少用)。
捕获多个异常不仅会使代码看起来更简单,还会更高效。生成的字节码只包含一个对应公共catch子句的代码块。

四、再次抛出异常与异常链

1、对原始异常做出改变的异常:

在catch子句中可以抛出一个异常,这样做的目的是改变异常的类型。如果开发了一个供其他程序员使用的子系统,用于表示子系统异常的类型解释也会有很多。ServletException就是这样一个异常例子。执行servlet的代码可能不想知道发生错误的细节,但希望知道servlet是否有问题。
捕获异常并将它再次抛出:

try
{
   access the database
}
catch (SQLException e)
{
   throws new ServletException("database error" + e.getMessage());
}

这里,ServletException用带有异常信息文本的构造器来构造。不过,可以有一种更好的处理方法,并且将原始异常设置为新异常的“原因”,即异常链:异常链是什么?指将捕获的异常包装进一个新的异常中并重新抛出的异常处理方式。

try
{
   access the database
}
catch (SQLException e)
{
    Throwable se = new ServletException("database error");
    se.initCause(e);  //对SQLException进行包装,为了出问题的时候能追根究底
    throw se;
}

当捕获到异常时,就可以使用 Throwable e = se.getCause();重新得到原始异常,强烈建议使用这种包装技术。这样可以让用户抛出子系统中的高级异常,而不会丢失原始异常的细节。如果在一个方法中发生了一个受查异常,而不允许抛出它,那么包装技术就十分有用。我们可以捕获这个异常,并将它包装成一个运行时异常。

try
{
     code
}
catch (IOException e)
{
    Throwable se = new RuntimeException("errorMessage");
    se.initCause(e);
    throw se;
}
2、不做任何改变

有时可能只能记录一个异常,再将它重新抛出而不做任何改变:

try
{
    access the database
}
catch (Exception e)
{
    logger.log(message,e);
    throws e;
}

五、finally子句

如果当代码抛出异常时,方法已经获得了一些本地资源,而这些本地资源在退出方法之前必须被回收,那么就会产生资源回收问题。一种解决方案就是捕获并重新抛出所有异常,但这需要在正常代码、异常代码两个地方清除所分配的资源。Java有一种更好的解决方案,就是finally子句。它可以恰当地关闭一个文件或关闭数据库的连接。try语句可以只有finally子句,而没有catch子句。
不管是否有异常被捕获,finally子句中的代码都会被执行。如:程序在所有情况下关闭文件:

InputStream in new FileInputStream(......);
try
{
    code that might throw exceptions  //1
}
catch(IOException e)
{
    error message  //2
}
finally
{
    in.close();  //3
}
//4

1、代码没有抛出异常,程序首先执行try语句块中的全部代码,然后执行finally子句中的代码,随后执行try语句块之后的第一语句。1-3-4
2、代码抛出catch子句中捕获的异常:
(1)如果catch子句没有抛出异常,程序将执行finally子句中的代码,随后执行try语句块之后的第一语句。1-2-3-4
(2)如果catch子句抛出一个异常,异常被抛出方法调用者,程序执行finally子句中的代码。1-2-3
3、代码抛出一个异常,但这个异常不是由catch子句捕获的。这种情况下,程序将执行try语句块中的所有语句,直到有异常被抛出为止。此时,程序执行finally子句中的代码,异常被抛出方法调用者。1-3

六、带资源的try语句

有时候,finally子句也会带来麻烦。比如,清理资源的方法也有可能抛出异常。还用上面的例子,执行finally子句时调用close方法,而close方法本身也有可能抛出IOException异常。当出现这种情况时,原始的异常将会丢失,抛出close方法的异常。这会有问题,因为第一个异常很可能更有意义。如果想做适当的处理,重新抛出原来的异常,代码会变得很繁琐。

InputStream in new FileInputStream(......);
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;
    }
}

Java中还有一种更好的方式,会使关闭资源的处理容易得多,对于以下代码模式:

open a resource
try
{
    work with the resource
}
finally
{
    close the resource
}

假设资源属于一个实现了AutoCloseable接口的类,Java为这种代码模式提供了一个很有用的快捷方式。AutoCloseable接口有一个方法:void close() throws Exception
带资源的try语句形式为如下所示,try语句块退出时,会自动调用res.close():

try(Resource res = ......)
{
    work with res
}

例如:读取一个文件中所有单词,这个语句块正常退出时,或者存在一个异常时,都会调用in.close()方法,就好像用了finally块一样。

try(Scanner in = new Scanner(new FileInputStream("filePath")), "UTF-8")
{
    while (in.hasNext());
}

如果用上述的常规方式,就需要嵌套的try/finally语句。所以只要关闭资源,就要尽可能使用带资源的try语句。
那么问题来了,如果try语句块抛出一个异常,close方法也抛出一个异常,怎么办呢?原来的异常会重新抛出,而close方法抛出的异常会“被抑制”。这些异常将自动捕获,并由addSuppressed方法增加到原来的异常,可以调用getSuppressed方法得到从close方法抛出并被抑制的异常列表。

上一篇下一篇

猜你喜欢

热点阅读