JAVA异常处理

异常处理不是OO的思想,或者说在OOP出现之前就已经有异常处理的概念,JAVA中的异常处理也很类似于面向过程,再将其封装在对象中。

1. 解决异常情形的基本思路

1.1 普通问题和异常情形

异常情形是指阻止当前方法或作用域继续执行的问题。普通问题是在编程的过程中,我们可以通过已知的信息解决,并继续执行的问题。而异常情形是指在当前的情况下,我们不能继续下去了,在当前的环境下我们不能解决这个问题,我们只能将这个问题抛出当前的环境,到一个大的环境中去企图解决这个问题。

1.2 抛出异常之后

如果我们没有能力处理的问题就需要抛出异常。在抛出异常的时候,会有几件事情发生:

  1. java用new在堆上来创建一个异常对象。
  2. 当前执行的程序不能被执行下去,程序被终止之后,从当前环境弹出一个对异常对象的引用。
  3. 异常处理机制接管程序,试图找到一个恰当的地方来执行异常处理。

异常处理使得我们可以将每件事情都看作一个事物来处理,而异常可以看作这些事务的底线。我们还可以将异常看作是一种内建的恢复系统,因为我们的程序中可以拥有各种不同的恢复点。如果程序的某部分事物都失败了,我们至少可以返回一个稳定的安全处。

异常和主线程互不干扰,是并发执行的。

2. 捕获异常

在抛出异常时用new在堆上创建异常对象,这伴随着储存空间的分配和构造器的调用。所有的标准异常类都有两个构造器:一个是默认的构造器,另一个是接受字符串作为参数。

2.1 将异常对象看作“返回”

关键词throw会产生很多有趣的结果。在使用new创建了异常对象之后,此对象的引用将会传递给throw。从效果上看,我们可以假装认为从这个方法或代码块“返回”了一个异常对象给throw。

请注意,throw是代码中抛出异常大关键字,throws是方法头中写在末尾标识此方法可能抛出什么异常的关键字。

2.2 try块

如果代码中抛出异常,那么我们的程序将会终止,如果不希望程序就此结束,我们可以通过try-catch块来操作。在try块中的内容如果抛出了异常,我们只会结束try块中运行的内容,而不会结束整个程序。

2.3 catch块

catch块就是异常处理程序。在try块内部,可能有几个方法会抛出同一个异常,我们只需写一个这种异常的catch块就可以捕获所有。而且,catch块要按照类型从小到大的顺序来写。可以用|来合并那些虽然异常不同,但是操作相同的catch块:

catch(FileNotFoundException | IOException e)
{
    //如果这两个异常的操作是一样的,我们可以把他们的操作写在一起,从jdk7开始
}

如上捕获多个异常的时候,异常变量隐含为final变量。不能为上面的代码的e赋不同的值。

2.4 创建一个自己定义的异常类

异常类有两种构造器方法,一种是默认的无参数构造方法,另外一种是传递一个String类型的构造器。

package ExceptionEx;

/**
 * 
 * @author QuinnNorris
 * 
 *         自定义异常类
 */
public class MyException extends Exception {

    public MyException() {

    }

    public MyException(String msg) {
        super(msg);
    }

}

创建一个测试类,在这个类中让一个方法通过throw抛出异常,并用try-catch块来接住这个异常。

package ExceptionEx;

/**
 * 
 * @author QuinnNorris
 * 
 *         测试类,用方法抛出异常
 */
public class TestExcep {

    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub

        try {
            throwMyExce();
        } catch (MyException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    public static void throwMyExce() throws MyException {
        throw new MyException();
    }

}

输出的结果是我们出错的栈轨迹,之所以会输出这些内容,是我们在catch块中调用printStackTrace方法的结果。

输出结果:
ExceptionEx.MyException
at ExceptionEx.TestExcep.throwMyExce(TestExcep.java:20)
at ExceptionEx.TestExcep.main(TestExcep.java:12)

3. 异常类继承关系树

3.1异常类关系图

java异常类关系树

3.2 Exception类和Error类

在java中,所有的异常都有一个共同的祖先Throwable类。Throwable类有两个子类(错误)Error和(异常)Exception。错误和异常有很大的区别。java中能通过代码处理的我们叫做异常,而我们不能处理的才叫做错误。错误指的是那些例如:JVM运行错误,栈空间用尽,类定义错误等等非常严重的问题。程序一旦出现Error是没办法解决的。比较常见的Error是大名鼎鼎的:OutOfMemoryError和StackOverflowError,即OOM和栈溢出。

3.3 运行时异常、非运行时异常

在Exception中又将异常分为两类,RunTimeException和其他的异常。其他的异常有很多种,但是我们为什么把运行时异常(RunTimeException)单独提出来作为一类呢?因为这里有本质性的区别。

RunTimeException表示那些逻辑性错误,可以避免。

运行时异常都包含那些呢?NullPointerException(空指针异常)、IndexOutOfBoundsException(下标越界异常)等。在编写代码时,我们不会希望编写出有空指针或者下标越界的代码,这些逻辑上因大意而造成的异常都叫做运行时异常RunTimeException。运行时异常是不会在程序中用try-catch块来声明的,因为如果出现这种错误我们会修改自己的代码纠正错误,而不是用try-catch块来捕获。

常见的运行时异常:
ClassCastException 类型转化异常
IndexOutOfBoundsException 数组越界异常
NullPointerException 空指针异常
ArithmeticException 算数运算异常

非运行时异常表示有可能发生的异常,我们需要声明。

编译器要求进行throws或者try-catch的异常是非运行时异常。为了防止代码在运行时出现问题,java强制规定:非运行时异常必须被处理。当要使用文件或者SQL语句的时候,出现例如文件找不到,数据库连接失败这样的问题很有可能,我们必须抛出或处理这种可能会出现的异常。

常见的非运行时异常:
FileNotFoundException 文件未找到
IOException 输入输出异常
SQLException 数据库异常
ParseException 数据格式转化异常

一句话来总结:运行异常是程序逻辑错误,无法预料,改正就可以,无需抛出或处理。非运行时异常是显然可能存在的错误,强制必须抛出或处理来保证程序的安全性。

3.4 已检查的异常、未检查的异常

通常,Java的异常(Throwable)又可以被分为已检查异常(checked exceptions)和未检查异常(unchecked exceptions)这两种。其实这两种和上面的差不多,这里给出概念:

已检查异常:
即非运行时异常。在程序中是可以被检查的,需要我们处理和预防的。

未检查的异常:
包括RuntimeException和Error。我们在程序和逻辑上尽量避免出现这种异常,如果出现这种异常是未知的。

4. Execption类

上面分析了Throwable类的两个子类,接下来分析一下Exception类的结构和可以使用的方法。

4.1 getMessage、getLocalizedMessage

刚才已经说过,Exception类有两种构造器,有一个String的参数的构造器可以用来存储错误信息。那么既然能够存储信息,也肯定有读取信息的方法。

public String getMessage()
//返回此 throwable 的详细消息字符串。 

public String getLocalizedMessage()
//创建此 throwable 的本地化描述。子类可以重写此方法,以便生成特定于语言环境的消息。
//对于不重写此方法的子类,默认实现返回与 getMessage() 相同的结果。 

我们通过这两种方法来获得存储着信息的字符串。

4.2 printStackTrace

printStackTrace这个方法有三种重载的形态,总的来说,这个方法的作用是输出错误信息。

public void printStackTrace()
//将此 throwable 及其追踪输出至标准错误流。
//此方法将此 Throwable 对象的堆栈跟踪输出至错误输出流,作为字段 System.err 的值。
//输出的第一行包含此对象的 toString() 方法的结果。
//剩余行表示以前由方法 fillInStackTrace() 记录的数据。

public void printStackTrace(PrintStream s)
//将此 throwable 及其追踪输出到指定的输出流。 

public void printStackTrace(PrintWriter s)
//将此 throwable 及其追踪输出到指定的 PrintWriter。 

我们给出一个API中的例子:

 class MyClass {
     public static void main(String[] args) {
         crunch(null);
     }
     static void crunch(int[] a) {
         mash(a);
     }
     static void mash(int[] b) {
         System.out.println(b[0]);
     }
 }

输出b[0]的时候,这个数组是不存在的,肯定是一个空指针的错误。通过上面的这个这个例子会产生以下的错误。

 java.lang.NullPointerException
         at MyClass.mash(MyClass.java:9)
         at MyClass.crunch(MyClass.java:6)
         at MyClass.main(MyClass.java:3)

4.3 fillInStackTrace

在上一个printStackTrace显示栈轨迹的方法中有说过,栈轨迹是存储在这个方法中的。

public Throwable fillInStackTrace()
//在异常堆栈跟踪中填充。此方法在 Throwable 对象信息中记录有关当前线程堆栈帧的当前状态。 

4.4 getStackTrace、setStackTrace

还有以数组的形式获得和修改栈轨迹的方法,但是在一般情况下几乎不会用到。

public StackTraceElement[] getStackTrace()
//提供编程访问由 printStackTrace() 输出的堆栈跟踪信息。
//返回堆栈跟踪元素的数组,每个元素表示一个堆栈帧。
//数组的第零个元素(假定数据的长度为非零)表示堆栈顶部,它是序列中最后的方法调用。

public void setStackTrace(StackTraceElement[] stackTrace)
//设置将由 getStackTrace() 返回,并由 printStackTrace() 和相关方法输出的堆栈跟踪元素。 
//此方法设计用于 RPC 框架和其他高级系统,允许客户端重写默认堆栈跟踪,

5. 使用finally进行清理

5.1 finally作用

对于一些代码,可能会希望无论try块中的异常是否抛出,它们都能得到执行。为了达到这个效果,可以在异常处理程序后面加上finally子句。对于没有垃圾回收和析构函数自动调用机制的语言来说,finally非常重要。它能使程序员保证:无论try块中发生什么,内存总能得到释放。一般使用finally子句关闭的资源包括:已经打开的文件或网络资源,在频幕上画的图形等等。

5.2 带资源的try块

在很多的情况下finally被使用只是为了简单的将资源关闭。jdk7为这种情况提供了一个很有用的快捷方式。可以为我们快速简洁的关闭用.close方法关闭的资源。

try(Resource res = ...)
{
    //do some work
}

在这种写法之下,try快退出时,会自动调用res.close()方法。省去了finally块的编写,但是这种写法仅仅在是“close”方法的时候可用,比如多线程中的ReentrantLock的关闭方式不是调用close方法,那这就不适用。

5.3 请简单的使用finally

遗憾的是,java中的异常实现有一些瑕疵。异常作为程序出错的标志,绝对不应该被忽略,如果被忽略会给调试者带来很大的麻烦。但是,请考虑这种情况:在try中调用了方法,这个方法抛出一个一场,但是在finally中又调用了其他的一个方法,这个新方法也抛出一个异常。理论上,当代码遇到异常的时候,会直接停止现在的工作,抛出异常,但是finally这种特殊的机制,导致又抛出了一个异常,而且这种抛出直接导致前面的异常被覆盖了。

甚至还有更令人绝望的问题,比如,你可以试着敲一下下面这段代码。

package ExceptionEx;

public class FinallyEx {

    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        try {
            throw new RuntimeException();
        } finally{
            return;
        }

    }

}

你会发现,这个程序不会产生任何输出。
这是一种相当严重的缺陷,因为异常可能会以一种比前面的例子更加微妙和难以察觉的方式完全丢失。在平时的项目中,我们要做的就是尽量少的把逻辑性代码放入finally中,finally最主要的作用还应该是关闭资源。

5.4 finally块中的代码一定会执行吗?

所有的书上都在说,finally块一定会被执行。但事实上finally块在一种情况下不会被执行:JVM被退出。虚拟机都被推出了,什么都不会执行了。

package ExceptionEx;

public class FinallyEx {

    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        try {
            System.exit(0); 

        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }finally{
            //do some thing;
            System.out.println("111");
        }

    }

}

如果调用System.exit(0)那么你会发现不会再打印出111这个字符串了。还有请不要在抛出异常后调用这个退出jvm的方法,编译器会报一个:Unreachable code(代码不会被执行)的错误。

6. 异常抛出的限制

如果在子类中覆盖了超类的一个方法,子类方法中声明的已检查异常不能比超类方法中声明的异常更通用。特别需要说明的是,如果超类方法没有抛出任何已检查异常,子类也不能抛出任何已检查异常。如果可以,请在子类中try-catch处理问题,如果不可以,让父类抛出throwable异常即可。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值