(转载)Java:异常的深入研究与分析

原文转载自:http://swiftlet.net/archives/998

Part1

前言

  本文是异常内容的集大成者,力求全面,深入的异常知识研究与分析。本文由金丝燕网独家撰写,参考众多网上资源,经过内容辨别取舍,文字格式校验等步骤编辑而成,以飨读者。对于本文的内容,建议小白需要多多思考力求掌握,对于老手只需意会温故知新。对于本文的内容,属于基础知识研究范畴,切勿以为读完此文就能将异常知识掌握到家。切记:操千曲而后晓声,观千剑而后识器,所以我觉得没有大量的源码阅读经验,你很难知道什么时候需要自定义异常,什么时候需要抛出异常。

异常机制概述

  异常机制是指当程序出现错误后,程序如何处理。具体来说,异常机制提供了程序退出的安全通道。当出现错误后,程序执行的流程发生改变,程序的控制权转移到异常处理器。

异常处理的流程

  当程序中抛出一个异常后,程序从程序中导致异常的代码处跳出,java 虚拟机检测寻找和 try 关键字匹配的处理该异常的 catch 块,如果找到,将控制权交到 catch 块中的代码,然后继续往下执行程序,try 块中发生异常的代码不会被重新执行。如果没有找到处理该异常的 catch 块,在所有的 finally 块代码被执行和当前线程的所属的 ThreadGroup 的 uncaughtException 方法被调用后,遇到异常的当前线程被中止。

异常的结构

  异常的继承结构:Throwable 为基类,Error 和 Exception 继承 Throwable,RuntimeException 和 IOException 等继承 Exception。Error 和 RuntimeException 及其子类成为未检查异常(unchecked),其它异常成为已检查异常(checked)。
这里写图片描述

Error 异常

  Error表示程序在运行期间出现了十分严重、不可恢复的错误,在这种情况下应用程序只能中止运行,例如 JAVA 虚拟机出现错误。Error 是一种 unchecked Exception,编译器不会检查 Error 是否被处理,在程序中不用捕获 Error 类型的异常。一般情况下,在程序中也不应该抛出 Error 类型的异常。

RuntimeException异常

  Exception 异常包括 RuntimeException 异常和其他非 RuntimeException 的异常。
  RuntimeException 是一种 Unchecked Exception,即表示编译器不会检查程序是否对 RuntimeException 作了处理,在程序中不必捕获 RuntimException 类型的异常,也不必在方法体声明抛出 RuntimeException 类。RuntimeException 发生的时候,表示程序中出现了编程错误,所以应该找出错误修改程序,而不是去捕获RuntimeException。

Checked Exception异常

  Checked Exception 异常,这也是在编程中使用最多的 Exception,所有继承自 Exception 并且不是 RuntimeException 的异常都是 checked Exception,上图中的 IOException 和 ClassNotFoundException。JAVA 语言规定必须对 checked Exception 作处理,编译器会对此作检查,要么在方法体中声明抛出 checked Exception,要么使用 catch 语句捕获 checked Exception 进行处理,不然不能通过编译。

在声明方法时候抛出异常

语法:throws(略)

为什么要在声明方法抛出异常?

  方法是否抛出异常与方法返回值的类型一样重要。假设方法抛出异常却没有声明该方法将抛出异常,那么客户程序员可以调用这个方法而且不用编写处理异常的代码。那么,一旦出现异常,那么这个异常就没有合适的异常控制器来解决。

为什么抛出的异常一定是已检查异常?

  RuntimeException 与 Error 可以在任何代码中产生,它们不需要由程序员显示的抛出,一旦出现错误,那么相应的异常会被自动抛出。遇到 Error,程序员一般是无能为力的;遇到 RuntimeException,那么一定是程序存在逻辑错误,要对程序进行修改;只有已检查异常才是程序员所关心的,程序应该且仅应该抛出或处理已检查异常。而已检查异常是由程序员抛出的,这分为两种情况:客户程序员调用会抛出异常的库函数;客户程序员自己使用 throw 语句抛出异常。
注意:
  覆盖父类某方法的子类方法不能抛出比父类方法更多的异常,所以,有时设计父类的方法时会声明抛出异常,但实际的实现方法的代码却并不抛出异常,这样做的目的就是为了方便子类方法覆盖父类方法时可以抛出异常。

在方法中如何抛出异常

语法:throw(略)

抛出什么异常?

  对于一个异常对象,真正有用的信息是异常的对象类型,而异常对象本身毫无意义。比如一个异常对象的类型是 ClassCastException,那么这个类名就是唯一有用的信息。所以,在选择抛出什么异常时,最关键的就是选择异常的类名能够明确说明异常情况的类。
  异常对象通常有两种构造函数:一种是无参数的构造函数;另一种是带一个字符串的构造函数,这个字符串将作为这个异常对象除了类型名以外的额外说明。

为什么要创建自己的异常?

  当 Java 内置的异常都不能明确的说明异常情况的时候,需要创建自己的异常。需要注意的是,唯一有用的就是类型名这个信息,所以不要在异常类的设计上花费精力。

throw 和 throws 的区别

public class TestThrow
{
    public static void main(String[] args)
    {
        try
        {
            //调用带 throws 声明的方法,必须显式捕获该异常
            //否则,必须在 main 方法中再次声明抛出
            throwChecked(-3);            
        }
        catch (Exception e)
        {
            System.out.println(e.getMessage());
        }
        //调用抛出 Runtime 异常的方法既可以显式捕获该异常,
        //也可不理会该异常
        throwRuntime(3);
    }
    public static void throwChecked(int a)throws Exception
    {
        if (a > 0)
        {
            //自行抛出 Exception 异常
            //该代码必须处于 try 块里,或处于带 throws 声明的方法中
            throw new Exception("a 的值大于 0,不符合要求");
        }
    }
    public static void throwRuntime(int a)
    {
        if (a > 0)
        {
            //自行抛出 RuntimeException 异常,既可以显式捕获该异常
            //也可完全不理会该异常,把该异常交给该方法调用者处理
            throw new RuntimeException("a 的值大于 0,不符合要求");
        }
    }
}

补充:throwChecked函数的另外一种写法如下所示:

public static void throwChecked(int a)
    {
        if (a > 0)
        {
            //自行抛出 Exception 异常
            //该代码必须处于 try 块里,或处于带 throws 声明的方法中
            try
            {
                throw new Exception("a 的值大于 0,不符合要求");
            }
            catch (Exception e)
            {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }

注意:此时在 main 函数里面 throwChecked 就不用 try 异常了。

应该在声明方法抛出异常还是在方法中捕获异常?

  处理原则:捕捉并处理哪些知道如何处理的异常,而传递哪些不知道如何处理的异常

使用 finally 块释放资源

  finally 关键字保证无论程序使用任何方式离开 try 块,finally 中的语句都会被执行。在以下三种情况下会进入 finally 块:

(1) try 块中的代码正常执行完毕。
(2) 在 try 块中抛出异常。
(3) 在 try 块中执行 return、break、continue。
  因此,当你需要一个地方来执行在任何情况下都必须执行的代码时,就可以将这些代码放入 finally 块中。当你的程序中使用了外界资源,如数据库连接,文件等,必须将释放这些资源的代码写入 finally 块中。
  必须注意的是:在 finally 块中不能抛出异常。JAVA 异常处理机制保证无论在任何情况下必须先执行 finally 块然后再离开try块,因此在 try 块中发生异常的时候,JAVA 虚拟机先转到 finally 块执行 finally 块中的代码,finally 块执行完毕后,再向外抛出异常。如果在 finally 块中抛出异常,try 块捕捉的异常就不能抛出,外部捕捉到的异常就是 finally块中的异常信息,而 try 块中发生的真正的异常堆栈信息则丢失了。
请看下面的代码:

Connection  con = null;
try
{
    con = dataSource.getConnection();
    ……
}
catch(SQLException e)
{
    ……
    throw e;//进行一些处理后再将数据库异常抛出给调用者处理
}
finally
{
    try
    {
        con.close();
    }
    catch(SQLException e)
{
    e.printStackTrace();
    ……
}
}

运行程序后,调用者得到的信息如下:

java.lang.NullPointerException at myPackage.MyClass.method1(methodl.java:266)

  而不是我们期望得到的数据库异常。这是因为这里的 con 是 null 的关系,在 finally 语句中抛出了 NullPointerException,在 finally 块中增加对 con 是否为 null 的判断可以避免产生这种情况。

丢失的异常

请看下面的代码:

public void method2()
{
try
{
    ……
    method1();  //method1 进行了数据库操作
}
catch(SQLException e)
{
    ……
    throw new MyException("发生了数据库异常:" + e.getMessage);
}
}
public void method3()
{
    try
{
    method2();
}
catch(MyException e)
{
    e.printStackTrace();
    ……
}
}

  上面 method2 的代码中,try 块捕获 method1 抛出的数据库异常 SQLException 后,抛出了新的自定义异常 MyException。这段代码是否并没有什么问题,但看一下控制台的输出:
MyException:发生了数据库异常:对象名称 ‘MyTable’ 无效。

at MyClass.method2(MyClass.java:232)
at MyClass.method3(MyClass.java:255)

  原始异常 SQLException 的信息丢失了,这里只能看到 method2 里面定义的 MyException 的堆栈情况;而 method1 中发生的数据库异常的堆栈则看不到,如何排错呢,只有在 method1 的代码行中一行行去寻找数据库操作语句了。
  JDK 的开发者们也意识到了这个情况,在 JDK1.4.1 中,Throwable 类增加了两个构造方法,public Throwable(Throwable cause) 和 public Throwable(String message,Throwable cause),在构造函数中传入的原始异常堆栈信息将会在 printStackTrace 方法中打印出来。但对于还在使用 JDK1.3 的程序员,就只能自己实现打印原始异常堆栈信息的功能了。实现过程也很简单,只需要在自定义的异常类中增加一个原始异常字段,在构造函数中传入原始异常,然后重载 printStackTrace 方法,首先调用类中保存的原始异常的 printStackTrace 方法,然后再调用 super.printStackTrace 方法就可以打印出原始异常信息了。可以这样定义前面代码中出现的 MyException 类:

import java.io.PrintStream;
import java.io.PrintWriter;
public class MyException extends Exception
{

    private static final long serialVersionUID = 1L;
    //原始异常
    private Throwable cause;
    //构造函数
    public MyException(Throwable cause)
    {
        this.cause = cause;
    }
    public MyException(String s,Throwable cause)
    {
        super(s);
        this.cause = cause;
    }
    //重载 printStackTrace 方法,打印出原始异常堆栈信息
    public void printStackTrace()
    {
        if (cause != null)
        {
            cause.printStackTrace();
        }
        super.printStackTrace();
    }

    public void printStackTrace(PrintStream s)
    {
        if (cause != null)
        {
            cause.printStackTrace(s);
        }
        super.printStackTrace(s);
    }

    public void printStackTrace(PrintWriter s)
    {
        if (cause != null)
        {
            cause.printStackTrace(s);
        }
        super.printStackTrace(s);
    }
}

Part2

本文主要是关于异常的面试题目,出自前几年的迅雷,支付宝等名企的笔试题目。内容由金丝燕网原创编辑,转载请注明链接。

题目一:考察异常类的继承结构

那个类是所有异常的基础类?
A String
B Error
C Throwable
D RuntimeException

参考答案:C

试题分析:

  在 Java 的 lang 包里面有一个 Throwable 类,它是所有异常的父类或者间接父类,它有两个直接子类:Error 和 Exception。Error 及其子类是处理系统内部及程序运行环境的异常,一般与硬件有关,由系统直接处理,不需要程序员在程序中处理。  Exception 又分两大类,运行时异常(RuntimeException)和非运行时异常。其中类 RuntimeException 代表运行时由 Java 虚拟机产生的异常,例如算术运算异常 ArithmeticException,数组越界异常 arrayIndexOutOfBoundsException 等;非运行时异常,例如输入输入异常 IOException 等,Java 编译器要求 Java 程序必须捕获或声明所有非运行时异常,但对运行时异常可以不做处理,因此,在编程时非运行时异常如果不处理,编译时会出错。

题目二:考察异常捕获的结构

分析下面给出的 Java 代码,编译运行后,输出的结果是什么?
public class print_message {
public static void main(String[] args) {
    print();
}    
static void print(){
    try{        
        System.out.println("thank you !");    
    }finally{    
        System.out.println("I am sorry !");        
    }
    }
}

A thank you !
B I am sorry !
C thank you !
  I am sorry !
D 代码不能编译

参考答案:C

试题分析:

  在Java中,try 和 catch 可以连用,try-catch-finally 可以连用,但是try,catch,finally 却不能单独使用,如果在程序中只想使用 try 而不想使用 catch 也可以,但是 try 的后面必须有 finally。
  在本题中,try 中的打印语句并没有异常发生所以正常输出,对于 finally 无论有没有异常发生,总是要执行的。

题目三:考察异常声明与抛出的区别

TimedOutException 不是一个 RuntimeException,下面的那些选项载入程序中,使程序可以正常运行?
A public void final()
B public void final() throws Exception
C public void final() throws TimedOutException
D public void final() throw TimedOutException
E public throw TimedOutException void final() 

参考答案:
BC

试题分析:

  如果一个程序在运行时候有异常发生,而这个异常又不是 RuntimeException 或者 Error,那么程序必须对这个异常进行捕获处理或者声明抛出该异常。捕获异常使用 try-catch-finally,而声明异常则是在声明方法的同时将会发生的异常进行声明,使用关键字 throws。
  A 项没有使用关键字声明异常,所以是错误的。由于 Exception 是所有异常的父类,当然也可以代表 TimedOutException,所以 B 项是正确的。C 项符合声明异常的格式,是正确的。在 D 项中,throw 是抛出异常,而不是声明异常,关键字使用错误,所以 D 项是错的。E 项的语法格式是错误的。

猜你喜欢

转载自blog.csdn.net/qq_33811662/article/details/80922300