Java异常总结
发布日期:2021-06-29 17:21:10 浏览次数:2 分类:技术文章

本文共 8361 字,大约阅读时间需要 27 分钟。

参考:《Java核心技术卷一》


目录


一、异常分类

在Java程序设计语言中,异常对象都是派生于Throwable类的一个实例。如果Java内置的异常类不能满足需求,用户也可以创建自定义的异常类,这个自定义异常类也派生于Throwable类。

                                                               图1-1  Java异常层次结构

1、错误(Error)、运行时异常(RuntimeException)和其他异常

图1-1是一个简化的Java异常层次结构。从图中可以看出,所有的异常都是由Throwable继承而来,但在下一层立即分解为两个分支:Error Exception

Error类层次结构描述了Java运行时系统的内部错误或资源耗尽错误,一般表示故障发生于虚拟机自身、或者发生在虚拟机试图执行应用时,如Java虚拟机运行错误(Virtual Machine Error)、类定义错误(NoClassDefFoundError)等。应用程序不应该抛出这种类型的错误对象。如果出现了这样的内部错误,除了通告给用户,并尽力使程序安全地终止之外,再也无能为力了。这种情况一般较少出现。

在设计Java程序时,需要关注Exception层次结构。这个层次结构又可分解为两个分支:一个分支派生于RuntimeException;另一个分支包含其他异常(即非RuntimeException)。划分这两个分支的规则是:由程序错误导致的异常属于RuntimeException;而程序本身没有问题,但由于像I/O错误这类程序不可控的问题导致的异常属于其他异常

具体来说,派生于RuntimeException的异常包含下面几种情况:

  • 错误的类型转换(ClassCastException);
  • 数组访问越界(ArrayIndexOutOfBoundsException);
  • 访问的对象是null(NullPointerException)。

可以说,“如果出现RuntimeException异常,那么一定是你写的程序出现了问题”,你应该检查你的程序来解决RuntimeException异常。比如我们应该通过检测数组下标是否越界来避免ArrayIndexOutOfBoundsException异常;应该通过在使用变量之前检测是否为null来杜绝NullPointerException异常的发生。

不是派生于RuntimeException的异常包括:

  • 试图在文件尾部后面读取数据(EOFException);
  • 试图打开一个不存在的文件(FileNotFoundException);
  • 试图根据给定的字符串查找对象,而这个字符串所表示的类并不存在(ClassNotFoundException)。

那么如何处理非运行时异常呢?比如应该如何处理不存在的文件?难道不能先检查文件是否存在再打开吗?确实是可以这样,但是这个文件可能在你检查完要读取前被删除了,因此,“是否存在”取决于环境,而不是取决于你的代码。

注意:上面所涉及的所有问题都是发生在程序运行时的,并不是只有RuntimeException异常才发生在程序运行时,不要被这个命名混淆了。

2、受检异常和非受检异常

除了上述异常类别,异常还可以分为受检异常非受检异常。一般来说,派生于 Error类或 RuntimeException类的所有异常称为非受检异常其他的所有异常称为受检异常。编译器将检查是否为所有的受检异常提供了异常处理器,也就是说,若某处代码可能出现受检异常,那么必须使用try/catch语句块捕获,或者使用throws语句进行声明

注意,一个方法必须声明或者捕获所有可能抛出的受检异常,而非受检异常要么不可控制(Error类),要么就应该避免出现(RuntimeException)。如果方法没有声明或捕获可能发生的受检异常,编译器会发出一个错误消息。

二、声明受检异常

如果遇到了无法处理的情况,那么Java方法可以抛出一个异常。这个道理很简单:一个方法不仅需要告诉编译器将要返回什么值,还要告诉编译器有可能发生什么错误。例如,一段读取文件的代码知道有可能读取的文件不存在,或者内容为空,因此,试图处理文件信息的代码就需要通知编译器可能会抛出IOException类的异常。

方法应该在其首部声明所有可能抛出的受检异常,具体的声明方式是通过throws语句,例如:

public static void main(String[] args) throws IOException{      ...}

如果一个方法有可能抛出多个受检异常类型,那么就必须在方法首部列出所有可能的异常类,每个异常类之间用逗号隔开,例如:

public static void main(String[] args) throws IOException, EOFException {     ...}

但是,方法首部不需要声明Error类错误,因为任何程序代码都具有抛出那些异常的潜能,而我们对其没有任何控制能力;同样,也不应该声明RuntimeException类异常,因为这些异常完全可以在我们控制之下避免,我们应该将更多的心思花费在修正程序中的这些错误代码,而不是用于声明这些错误发生的可能性上。

当然,第二部分提到了,除了声明异常之外,还可以捕获异常,这样就会使得异常不被抛到方法之外,也就不需要throws规范了。

警告:如果在子类中覆盖了父类的一个方法,子类方法中声明的受检异常不能比父类方法中声明的异常更通用(也就是说,子类方法中可以抛出更特定的异常,或者根本不抛出异常)。特别需要说明的是,如果父类方法没有抛出任何受检异常,则子类也不能抛出任何异常。

三、抛出异常

假设在程序代码中发生了一些糟糕的事情,例如,一个名为readData的方法正在读取一个首部具有下列信息的文件:

Content-length:1024

然而,读到733个字符之后文件就结束了,我们认为这是一种不正常的情况,希望抛出一个异常,如何进行抛出异常的操作呢?

首先要决定应该抛出什么类型的异常。在上述例子中,将异常归结为IOException是一种很好的选择。阅读Java API文档可以发现:EOFException异常描述的是“在输入过程中,遇到了一个未预期的EOF后的信号”。所以,这正是我们要抛出的异常,需要用throw关键字来抛出这个异常,具体形式有两种:

throw new EOFException();

EOFException e = new EOFException();throw e;

综上,可以看出,抛出一个异常的过程主要有三步:

  1. 找到一个合适的异常类;
  2. 创建这个类的一个实例对象;
  3. 利用throw关键字将这个对象抛出。

一旦方法抛出了异常 ,这个方法就不可能返回到调用者。也就是说,不必为返回的默认值或错误代码担忧。

四、捕获异常

上面介绍了抛出异常,这个过程十分简单,只要将其抛出就不用理睬了。但是,如果某个异常发生的时候没有在任何地方进行捕获,那程序就会终止执行,并在控制台上打印出异常信息,包括异常的类型和堆栈的内容,所以有时候就需要捕获异常,避免程序终止。

try/catch语句块

要想捕获一个异常,必须设置 try/catch语句块。最简单的 try/catch语句块如下所示:

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

如果在try语句块中的任何代码抛出了一个在catch子句中说明的异常类,那么

1)程序将跳过try语句块中的剩余代码;

2)程序将执行catch子句中的处理器代码。

如果方法中的任何代码抛出了一个在catch子句中没有声明的异常类型,那么这个方法就会立刻退出。

捕获多个异常

在一个try语句块中可以捕获多个异常类型,并对不同类型的异常做出不同的处理。如下例所示,可以按照这种方式为每个异常类型使用一个单独的catch子句:

try{     code that might throw exceptions;}catch(FileNotFoundException e){     emergency action for missing files;}catch(UnknownHostException e){     emergency action for unknown hosts;}catch(IOException e){     emergency action for all other I/O problems;}

finally子句

当代码抛出一个异常时,就会终止方法中剩余代码的处理,并退出这个方法的执行。如果方法获得了一些本地资源,并且只有这个方法自己知道,又如果这些资源在退出方法之前必须被回收,那么就会产生资源回收问题。一种解决方案是捕获并重新抛出所有的异常,但这种方案比较繁琐,因为需要在两个地方清除所分配的资源,一个在正常的代码中,另一个在异常代码中。因此,Java提供了一种更好的解决方案,那就是finally子句。不管是否有异常被捕获,finally子句中的代码都将被执行。比如读取文件结束后,需要恰当地关闭一个文件;比如使用Java编写数据库程序 ,也需要使用同样的技术关闭与数据库的连接。下面示例中,程序将在所有情况下关闭文件:

FileInputStream fis = new FileInputStream("...");try{     //1     code that might throw exceptions;     //2}catch (IOException e){     //3     show error messages;     //4}finally {     //5     fis.close();}//6

在上面这段代码中,程序的执行情况如下:

1)代码没有抛出异常。这种情况下,程序首先执行try语句块中的全部代码,然后执行finally子句中的代码,随后继续执行try语句块之后的第一条语句。也就是说,执行标注的1、2、5、6处。

2)代码抛出一个在catch子句中捕获的异常,上面示例中就是IOException异常。这种情况下,程序将按序执行try语句块中的代码,直到发生异常为止。发生异常后,程序将跳过try语句块中的剩余代码,转去执行与该异常匹配的catch子句中的代码,最后执行finally子句中的代码。如果catch子句没有抛出异常,那么程序将执行try语句块之后的第一条语句,也就是执行标注的1、3、4、5、6;如果catch子句抛出了一个异常,那么异常将会被抛回这个方法的调用者,也就是说,执行标注1、3、5处的语句。

3)代码抛出了一个异常,但这个异常不是由catch子句捕获的,这种情况下,程序将按序执行try语句块中的代码,直到有异常被抛出为止。异常发生后,程序将跳过try语句块中的剩余代码,然后执行finally子句中的语句,并将异常抛给这个方法的调用者。也就是说,执行标注1、5处的语句。

注意,try语句可以只有finally子句而没有catch子句,不管try语句中是否遇到异常,正常情况下,finally子句中的语句都会被执行。不过,也有一些情况下,finally子句不会执行:

  • 在 try 或 finally块中用了 System.exit(int)退出程序。但是,如果 System.exit(int) 在异常语句之后,finally 还是会被执行;
  • 程序所在的线程死亡;
  • CPU被关闭了。

这里还有一个要注意的点:当try语句块中包含return语句的返回值情况。假设利用return语句从try语句块中退出,在方法返回前,finally子句中的内容将会被执行;如果finally子句中也有return语句,这个return语句的返回值将会覆盖try语句块中的返回值。下面以例子来说明:

public static int f(int n){     try{         int r = n * n;         return r;     }finally {         if(n == 2){             return 0;         }     }}

如果调用f(2),那么try语句块的计算结果为4,并执行return语句。然而,在方法真正返回前,还要执行finally子句,finally子句使得方法返回0,这个返回值覆盖了原先的返回值4。

五、带资源的try语句(try-with-resources)来代替 try-catch-finally

《Effecitve Java》中明确指出:

“面对必须要关闭的资源,我们总是应该优先使用 try-with-resources 而不是try-finally。随之产生的代码更简短、更清晰,产生的异常对我们也更有用。try-with-resources语句让我们更容易编写必须要关闭的资源的代码,若采用try-finally则几乎做不到这点。”

假设某个资源属于一个实现了AutoCloseable接口Closeable接口的类,Java7为这种资源的关闭提供了一种新的、便捷的方式。在这之前,Java 中类似于InputStream、OutputStream 、Scanner 、PrintWriter等的资源都需要我们调用close()方法来手动关闭,一般情况下我们都是通过try-catch-finally语句来实现这个需求。但是Java7提供的try-with-resources方法的最简形式为:

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

try块退出时,会自动调用res.close()来关闭res资源

举一个具体的例子:

//读取文本文件的内容Scanner scanner = null;try {      scanner = new Scanner(new File("D://read.txt"));      while (scanner.hasNext()) {           System.out.println(scanner.nextLine());      }} catch (FileNotFoundException e) {      e.printStackTrace();} finally {      if (scanner != null) {            scanner.close();      }}

使用 Java 7 之后的 try-with-resources 语句改造上面的代码:

try (Scanner scanner = new Scanner(new File("test.txt"))) {    while (scanner.hasNext()) {        System.out.println(scanner.nextLine());    }} catch (FileNotFoundException fnfe) {    fnfe.printStackTrace();}

这个块正常退出时或者存在一个异常时,都会自动调用scanner.close()方法,就好像使用了finally块一样。

此外, try-with-resources还可以自动关闭多个资源,只要通过使用分号分隔,可以在 try-with-resources块中声明多个资源,比如:

try (BufferedInputStream bin = new BufferedInputStream(new FileInputStream(new File("test.txt")));     BufferedOutputStream bout = new BufferedOutputStream(new FileOutputStream(new File("out.txt")))) {         int b;         while ((b = bin.read()) != -1) {              bout.write(b);         }}catch (IOException e) {         e.printStackTrace();}

因此,当代码涉及到需要关闭资源时,应该优先使用 try-with-resources方式。

六、自定义异常类

在程序中,可能会遇到任何标准异常类都不能充分描述清楚的问题,这种情况下,创建一个自定义异常类就是一件顺理成章的事了。我们需要做的只是定义一个派生于Exception的类或者派生于Exception子类的类。习惯上,定义的类应该包含两个构造器,一个是默认的构造器,另一个是带有详细描述信息的构造器(一般通过超类Throwable的toString()方法打印详细信息)。

下面定义一个继承IOException类的自定义异常类:

class  FileFormatException extends IOException{    public FileFormatException(){ }    public FileFormatException(String gripe){        super(gripe);    }}

定义完之后,就可以抛出自己定义的异常类了。

七、Java中的常见异常

1、Error类及其子类(非受检异常)

序号 异常名称 异常解释 异常出现可能原因
1 OutOfMemoryError 内存溢出
2 StackOverflowError 栈溢出 请求栈的深度超过虚拟机栈所允许的最大深度
3 NoClassDefFoundError 运行时加载类出错 一般发生在运行时需要加载对应的类不成功,其具体介绍以及和ClassNotFoundException的区别可以参考 和 

2、RuntimeException及其子类下属异常(非受检异常):

序号 异常名称 异常解释 异常出现可能原因
1 ArrayIndexOutOfBoundsException 数组索引越界异常 当对数组的索引值为负数或大于等于数组大小时抛出。
2 NullPointerException 空指针异常 当试图在要求使用对象的地方使用了null时,抛出该异常。如:调用null对象的实例方法、访问null对象的属性、计算null对象的长度、使用throw语句抛出null等等。
3 NumberFormatException 数据类型转换异常 当要将字符串类型的数据转化为数字类型的数据时,字符串没有使用恰当的格式。
4 ClassCastException 类型转换异常 通常是进行强制类型转换时候出现的异常。详细介绍可参考
5 ArithmeticException   算术条件异常 一般发生在算术运算过程中,比如整数除以0等。
6 IllegalArgumentException  参数错误异常 一般发生在向方法传递了一个不合法或不正确的参数时

3、其他Exception(受检异常):

(1)IOException及其子类下属异常:

序号 异常名称 异常解释 异常出现可能原因
1 IOException 操作输入流和输出流时可能出现的异常
2 EOFException 文件读取异常 当读取文件时实际上已经读到文件尾了,但是程序不知道,还在继续读,此时会抛出这个异常。详细介绍可参考
3 FileNotFoundException 文件未找到异常 当定位要读取的文件时可能会抛出这个异常,一般可以分为两种原因:(1)系统找不到指定的文件;(2)拒绝访问。具体可参考

(2)

序号 异常名称 异常解释 异常出现可能原因
1 ClassNotFoundException 操作输入流和输出流时可能出现的异常 一般发生在编译期应用程序试图加载相应的类时,找不到对应的类。

下面是一个Java异常的大致结构体系,供参考:

转载地址:https://blog.csdn.net/m0_37415978/article/details/116172145 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:操作系统内存管理
下一篇:操作系统进程管理

发表评论

最新留言

初次前来,多多关照!
[***.217.46.12]2024年04月27日 08时28分30秒