异常处理

异常处理概述

异常(Exception)是程序在执行过程中所产生的问题。导致异常的产生的原因有很多种,包括:用户输入了无效的数据、找不到一个需要打开的文件、在通讯过程中网络连接断开或者JVM发生了内存溢出等等。

有些异常是由于用户的错误所导致的,有些是由程序员的错误导致的,有些则是由硬件设备的故障导致的。在本章中,我们将详细介绍不同类型的异常,以及在什么时候应该抛出一个异常,在什么时候应该捕获一个异常,如何编写和抛出自定义的异常。

为了更好地认识和理解Java语言中异常处理的工作机制,我们首先需要认识异常的三个种类:

  • 检查异常:检查异常通常是用户错误或者不能被程序员所预见的问题。例如,如果要打开一个文件,但却无法找到该文件,此时就会产生异常。这种类型的异常被称为检查异常,它必须用Java语言来处理,而不能被简单的忽略。在后面介绍异常的处理和声明的规则时,我们将看到这种类型的异常。
  • 运行时异常:运行时异常是一个程序在运行过程中可能发生的、可以被程序员避免的异常类型。与检查异常不同的是,运行时异常可以被忽略。在程序开发时,我们应该让运行时异常使程序崩溃,然后找到问题所在,并更改代码,以使得异常不会再次发生。运行时异常的例子包括:数组越界、除数为零、引用为null、把引用类型转换为一个无效的数据类型等。
  • 错误:实际上,错误根本不是异常,但却是用户或程序员所无法控制的问题。错误通常在我们的代码中被忽略,虽然我们想在程序中来修复这个问题,但我们对一个错误却很少能有所作为。例如,如果发生调用栈溢出,将会导致一个错误。然而,由于内存不足,我们的程序将无法继续执行。我们所编写的任何程序代码都无法解决这一问题。因此,这样的错误通常在设计和编写Java应用程序时被忽略。

虽然错误不是异常,但是错误在程序控制流程中发生时类似于异常。异常和错误都能够使得我们的应用程序崩溃。接下来我们将详细介绍异常的控制和处理。

异常的控制流程

在Java语言中,异常(Exception)是被一个方法抛出的对象。当一个方法被调用时,这个方法被压入到内存的方法调用栈中。当一个方法抛出异常时,该方法从调用栈中被弹出,同时产生的异常对象抛给了栈中的前一个方法。例如,假如应用程序的main()方法在调用栈的底部,接着是method1()和method2()方法。如果method2()方法抛出一个异常,method2()方法就会从调用栈中被取出,同时异常抛给了下面的method1()方法。

对于这个异常的处理,method1()方法有三种选择:

  • 捕获这个异常,不让它沿着调用栈继续向下抛出;
  • 捕获这个异常,并继续向下抛出;
  • 不捕获这个异常,从而导致method1()方法从调用栈中被弹出,异常对象继续抛给调用栈下面的main()方法。

不论调用栈中有多少方法,程序控制流程都将继续在调用栈中向下执行。在调用栈中的每一个方法要么捕获并处理这个异常,要么捕获这个异常并再次抛出,或者什么都不做让异常进入下一个方法。

当这个异常到达调用栈的底部时会发生什么情况呢?如果Exception对象被抛给了main()方法,那么main()方法最好捕获该异常,否则程序就会终止。当一个异常到达调用栈的底部,如果没有方法来处理它,JVM将会崩溃,并且通知我们这个异常的详细情况。

Thowable类

异常有三种不同的类型,但它们都拥有一个公共的父类:java.lang.Throwable。只有Throwable类型的对象能够被JVM抛出。Throwable类有两个子类:Exception和Error。

Java语言中异常的继承层次关系就是基于这三个类。Error类是所有Java错误类的父类;Exception类是所有异常的父类,包括运行时异常和检查异常。

在这种继承层次关系中,运行时异常和检查异常根据适合的不同情况作出了进一步的区分。如果一个类是RuntimeException类的子类,那么这个子类代表了一个运行时异常。如果一个类是Exception的子类,但并不是RuntimeException的子类,那么这个类就是一个检查异常。

例如,ArrayIndexOutOfBoundsException和ArithmeticException是运行时异常,因为它们都是RuntimeException的子类。IOException和ClassNotFoundException是检查异常,因为它们都是Exception的子类。

Exception对象是Throwable类型的Java对象。当我们捕获到异常的时候,将获得一个指向Throwable类型对象的引用。每一个异常类都是不同的,并且拥有自己特定的方法。但是,由于所有的异常都继承于Throwable类,所以我们还可以在任何被捕获的异常上调用Throwable类的方法。

如下是Throwable类的一些方法的描述,更详细的方法描述请参考Java API文档:

  • public String getMessage():返回关于已发生的异常的详细信息。这个信息在Throwable类的构造方法中被初始化。
  • public Throwable getCause():返回Throwable对象所描述的异常产生的原因。这个原因由Throwable类的构造方法或initCause()方法进行初始化;
  • public String toString():返回Throwable类的简短描述;
  • public void printStackTrace():把toString()方法的结果与调用栈跟踪信息一起打印在控制台错误输出流中(System.err是Java程序运行在Windows窗口上的命令提示符)。为发送调用栈跟踪信息到指定输出流,该方法进行了重载。
  • public StackTraceElement [] getStackTrace():返回一个包含调用栈跟踪信息每个元素的数组。索引号为0的元素表示调用栈的顶部,数组中的最后一个元素表示调用栈底部的方法。此方法允许我们应用程序以编程的方式遍历调用栈的每一行。
  • public Throwable fillInStackTrace():使用当前调用栈跟踪信息来填充Throwable对象的调用栈跟踪信息,并添加以往任何信息到调用栈跟踪信息里。

捕获异常

在Java语言中,我们通常在一个方法中使用try和catch关键字来捕获异常。使用try/catch关键字的代码块把可能产生异常的代码“包围起来”,其中的代码也被称为“被保护的代码”。

catch语句包含我们想要捕获的异常的类型声明。如果在“被保护的代码”中发生异常,try块后面的catch块就会尝试对这个异常进行检查。如果发生的异常类型是在catch语句中所罗列出来的,那么异常对象就像方法的参数一样传递给catch块中。

需要注意的是,一个try/catch块并不能捕获一切。例如,如果我们想捕获一个NullPointerException异常,但是却发生了一个ArithmeticException异常,此时ArithmeticException就不能被捕获。

多个catch块

单个try块后可以有任意多个catch块。如果在“被保护的代码”中有异常产生,那么这个异常就会抛给下面的第一个catch块。如果抛出的这个异常的数据类型匹配,那么异常将在这里被捕获。如果不匹配,异常将继续向下传递给第二个catch块。这种情况将持续下去,直到异常被捕获或者通过所有的catch块。在这种情况下,目前的方法停止执行,并且异常向下抛出到调用栈前面的方法中。

异常捕获与多态性

虽然一个try块后可以跟随多个catch块,但是catch块不能简单地以任意顺序列出。当异常发生的时候,catch块会依照它们排列的顺序被依次检查。由于多态性的存在,一个catch块有可能不会被检查到。

异常处理及声明的规则

在Java语言中对于检查异常有一个严格执行的规则,这个规则被称为异常处理和声明规则。这个规则指出一个检查异常要么被处理,要么被声明。处理异常是指异常的捕获,而声明异常是指一个方法在方法签名时使用throws关键字,但是声明的异常在该方法中不会被处理。

必须注意的是:异常处理和声明规则不适用于运行时异常。如果程序导致了运行时异常,我们可以选择捕获这个异常,或者干脆忽略它使得程序崩溃。如前所述,在多数情况下,不要试图去捕获运行时异常,因为它们往往都是由于糟糕的代码设计所造成的。解决的办法是让运行时异常使程序崩溃,然后发现问题并解决。

我们想强调运行时异常与检查异常之间的区别,并解释为什么运行时异常没有遵守异常处理和声明的规则。例如,使用点运算符来访问对象的方法或属性,如果对象引用是null,那就可能产生一个NullPointerException。而NullPointerException类是RuntimeException的子类,因此它也是一个运行时异常。如果每次在使用点运算符的时候都要试图去捕获NullPointerException的话,那么代码中将会包含很多的try/catch块。幸好我们可以在代码中忽略潜在的运行时异常。

但是,假如我们试图打开一个文件,但是这个文件却并不存在,如果我们简单忽略文件不存在的这个事实,那么程序的其它部分如何避免受到影响呢?在Java语言中,我们不能够忽略像文件没找到这种情形。我们必须处理潜在的检查异常,否则Java代码就不能通过编译。

声明异常

如果一个方法没有处理检查异常,那么该方法必须使用throws关键字来声明异常。关键字throws出现在方法签名的末尾。

抛出异常

我们可以通过使用throw关键字来抛出异常,这个异常可以是一个新的异常实例,也可以是一个我们刚刚捕获的异常。throw语句将导致当前代码立即停止执行,而且异常将被抛给调用栈中的前一个方法。

finally关键字

关键字finally用于在try块后创建一个代码块。finally代码块总是会执行,不管异常是否发生。我们可以使用finally块来执行清理类型的语句,而不管被保护的代码中发生了什么。

用户自定义异常

在Java中,我们可以创建自定义异常。实际上,因为Java被设计的方式,我们被鼓励来编写自定义的异常,以代表在我们的类中会出现的问题。在编写自定义异常类时,必须牢记如下几点:

  • 所有的异常必须是Throwable的子类;
  • 如果我们想编写一个可以自动被异常处理或声明规则强制的检查异常,就需要继承Exception类;
  • 如果想编写一个运行时异常,就需要继承RuntimeException类。

我们肯定不会编写一个直接继承Throwable类的类,因为此后这个类既不是检查异常,也不是运行时异常。大多数用户自定义的异常类都被设计为检查异常,因而会继承Exception类。但是,如果我们想编写一个不想让用户处理或声明的异常,就应该通过继承RuntimeException类来使它成为一个运行时异常。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值