Java:详解Java中的异常(Error与Exception)

一、 异常机制的概述

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

程序错误分为三种:1.编译错误;2.运行时错误;3.逻辑错误。

      (1)编译错误是因为程序没有遵循语法规则,编译程序能够自己发现并且提示我们错误的原因和位置,这个也是大家在刚接触编程语言最常遇到的问题。
      (2)运行时错误是因为程序在执行时,运行环境发现了不能执行的操作。
      (3)逻辑错误是因为程序没有按照预期的逻辑顺序执行。异常也就是指程序运行时发生错误,而异常处理就是对这些错误进行处理和控制。

二、 异常的结构    

       在 Java 中,所有的异常都有一个共同的祖先 Throwable(可抛出)。Throwable 指定代码中可用异常传播机制通过 Java 应用程序传输的任何问题的共性。

Throwable: 有两个重要的子类:Exception(异常)和 Error(错误),二者都是 Java 异常处理的重要子类,各自都包含大量子类。异常和错误的区别是:异常能被程序本身可以处理,错误是无法处理。 

Throwable类中常用方法如下:

1. 返回异常发生时的详细信息
public string getMessage();
 
2. 返回异常发生时的简要描述
public string toString();
 
3. 返回异常对象的本地化信息。使用Throwable的子类覆盖这个方法,可以声称本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与getMessage()返回的结果相同
public string getLocalizedMessage();
 
4. 在控制台上打印Throwable对象封装的异常信息
public void printStackTrace();

Error(错误):是程序无法处理的错误,表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。例如,Java虚拟机运行错误(Virtual MachineError),当 JVM 不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止。这些错误表示故障发生于虚拟机自身、或者发生在虚拟机试图执行应用时,如Java虚拟机运行错误(Virtual MachineError)、类定义错误(NoClassDefFoundError)等。这些错误是不可查的,因为它们在应用程序的控制和处理能力之 外,而且绝大多数是程序运行时不允许出现的状况。对于设计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。在 Java中,错误通过Error的子类描述。 

Exception(异常):是程序本身可以处理的异常。Exception 类有一个重要的子类RuntimeException。RuntimeException 类及其子类表示“JVM 常用操作”引发的错误。例如,若试图使用空值对象引用、除数为零或数组越界,则分别引发运行时异常(NullPointerException、ArithmeticException)和 ArrayIndexOutOfBoundException。

Exception(异常)分两大类:运行时异常非运行时异常(编译异常)。程序中应当尽可能去处理这些异常。       

                1.运行时异常:都是RuntimeException类及其子类异常,如NullPointerException(空指针异常)、IndexOutOfBoundsException(下标越界异常)等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。运行时异常的特点是Java编译器不会检查它,也就是说,当程序中可能出现这类异常,即使没有用try-catch语句捕获它,也没有用throws子句声明抛出它,也会编译通过。
              2.非运行时异常 (编译异常):是RuntimeException以外的异常,类型上都属于Exception类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException、SQLException等以及用户自定义的Exception异常,一般情况下不自定义检查异常。

通常,Java的异常(Throwable)分为可查的异常(checked exceptions)不可查的异常(unchecked exceptions)

1. 可查异常(编译器要求必须处置的异常):正确的程序在运行中,很容易出现的、情理可容的异常状况。除了Exception中的RuntimeException及RuntimeException的子类以外,其他的Exception类及其子类(例如:IOException和ClassNotFoundException)都属于可查异常。这种异常的特点是Java编译器会检查它,也就是说,当程序中可能出现这类异常,要么用try-catch语句捕获它,要么用throws子句声明抛出它,否则编译不会通过。

 2. 不可查异常(编译器不要求强制处置的异常):包括运行时异常(RuntimeException与其子类)和错误(Error)。RuntimeException表示编译器不会检查程序是否对RuntimeException作了处理,在程序中不必捕获RuntimException类型的异常,也不必在方法体声明抛出RuntimeException类。RuntimeException发生的时候,表示程序中出现了编程错误,所以应该找出错误修改程序,而不是去捕获RuntimeException。

三、 异常处理的机制

在 Java 应用程序中,异常处理机制为:抛出异常捕捉异常

1. 抛出异常:当一个方法出现错误引发异常时,方法创建异常对象并交付运行时系统,异常对象中包含了异常类型和异常出现时的程序状态等异常信息。运行时系统负责寻找处置异常的代码并执行。

       任何Java代码都可以抛出异常,如:自己编写的代码、来自Java开发环境包中代码,或者Java运行时系统。无论是谁,都可以通过Java的throw语句抛出异常。从方法中抛出的任何异常都必须使用throws子句。

3.1、throws抛出异常

       如果一个方法可能会出现异常,但没有能力处理这种异常,可以在方法声明处用throws子句来声明抛出异常。例如汽车在运行时可能会出现故障,汽车本身没办法处理这个故障,那就让开车的人来处理。

       throws语句用在方法定义时声明该方法要抛出的异常类型,如果抛出的是Exception异常类型,则该方法被声明为抛出所有的异常。多个异常可使用逗号分割。throws语句的语法格式为:        

 methodname throws Exception1,Exception2,..,ExceptionN  {  }  

       方法名后的throws Exception1,Exception2,…,ExceptionN 为声明要抛出的异常列表。当方法抛出异常列表的异常时,方法将不对这些类型及其子类类型的异常作处理,而抛向调用该方法的方法,由他去处理。

       使用throws关键字将异常抛给调用者后,如果调用者不想处理该异常,可以继续向上抛出,但最终要有能够处理该异常的调用者。

 Throws抛出异常的规则:

1:  如果是不可查异常(unchecked exception),即Error、RuntimeException或它们的子类,那么可以不使用throws关键字来声明要抛出的异常,编译仍能顺利通过,但在运行时会被系统抛出
2:  如果一个方法可能出现可查异常(checked exception),要么用try-catch语句捕获,要么用throws子句声明将它抛出,否则会导致编译错误。
3:  只有当抛出了异常时,该方法的调用者才必须处理或者重新抛出该异常。当方法的调用者无力处理该异常的时候,应该继续抛出。
4:  调用方法必须遵循任何可查异常的处理和声明规则。若覆盖一个方法,则不能声明与覆盖方法不同的异常。声明的任何异常必须是被覆盖方法所声明异常的同类或子类。

例子1

package com.softeem.wolf.exception;

/**
 * Created by 苍狼
 * Time on 2021-10-28
 */
public class ExceptionTest02 {
    public static void main(String[] args) {
        System.out.println("程序执行开始的地方...");
        try{
            method1();
        } catch (ArithmeticException e){
            System.out.println("我来解决这个问题了...");
        } finally {
            System.out.println("我是main中finally执行的代码.....");
        }

        System.out.println("main方法执行的最后一个方法....");
    }

    public static void method1() throws ArithmeticException{
        int a = 10;
        int b = 0;
        System.out.println("进入异常的开头....");
        System.out.println(a/b);
        System.out.println("try结束所执行的.....");
        System.out.println("正常执行");
        System.out.println("我是method1()最后执行的代码....");
    }
}

执行结果

程序执行开始的地方...
进入异常的开头....
我来解决这个问题了...
我是main中finally执行的代码.....
main方法执行的最后一个方法....

代码分析: 首先main方法开始执行, 执行第一个语句, 然后调用method1()方法, 但执行到System.out.println(a/b)时报错, 但是method1()方法并没有解决这个异常, 所以method1()后面的代码终止执行, 回到main方法中, main方法将其解决了, 执行了catch{}里面语句, 最后finally{}语句, 最后执行main方法最后的语句.

例子2

package com.softeem.wolf.exception;

/**
 * Created by 苍狼
 * Time on 2021-10-28
 */
public class ExceptionTest02 {
    public static void main(String[] args) throws ArithmeticException{
        System.out.println("程序执行开始的地方...");
        try{
            method1();
        } finally {
            System.out.println("我是main中finally执行的代码.....");
        }

        System.out.println("main方法执行的最后一个方法....");
    }

    public static void method1() throws ArithmeticException{
        int a = 10;
        int b = 0;
        System.out.println("进入异常的开头....");
        System.out.println(a/b);
        System.out.println("try结束所执行的.....");
        System.out.println("正常执行");
        System.out.println("我是method1()最后执行的代码....");
    }
}

运行结果

Exception in thread "main" java.lang.ArithmeticException: / by zero
    at com.softeem.wolf.exception.ExceptionTest02.method1(ExceptionTest02.java:23)
    at com.softeem.wolf.exception.ExceptionTest02.main(ExceptionTest02.java:11)
程序执行开始的地方...
进入异常的开头....
我是main中finally执行的代码.....

代码分析: 首先也是执行main方法的开始语句, 后面跳到method1()中, 执行到了System.out.println(a/b)语句时, 发现异常, 但是method1()并没有将他解决, 所以method1()方法终止执行, 回跳到main方法中, 但是main()方法也没有对他进行解决, 所以main方法也结束了, 不继续执行了. 但是由于finally的存在, 所以它得执行完finally{}语句之后在结束.

3.2、使用throw抛出异常

throw总是出现在方法体中,用来抛出一个Throwable类型的异常。程序会在throw语句后立即终止,它后面的语句执行不到,然后在包含它的所有try块中(可能在上层调用函数中)从里向外寻找含有与其匹配的catch子句的try块。

我们知道,异常是异常类的实例对象,我们可以创建异常类的实例对象通过throw语句抛出。该语句的语法格式为:

throw new exceptionname;

例如抛出一个IOException类的异常对象:

throw new IOException;

要注意的是,throw 抛出的只能够是可抛出类Throwable 或者其子类的实例对象。下面的操作是错误的,因为String 不是Throwable 类的子类。

throw new String("exception");

如果抛出了可查异常,则还应该在方法头部声明方法可能抛出的异常类型。该方法的调用者也必须检查处理抛出的异常。

如果所有方法都层层上抛获取的异常,最终JVM会进行处理,处理也很简单,就是打印异常消息和堆栈信息。如果抛出的是Error或RuntimeException,则该方法的调用者可选择处理该异常。

throw的示例

package com.softeem.wolf.exception;

/**
 * Created by 苍狼
 * Time on 2021-10-28
 */
public class ExceptionTest02 {
    public static void main(String[] args) throws ArithmeticException{
        System.out.println("程序执行开始的地方...");
        try{
            method1();
            //当这里有异常发生时, 后面的代码也都不会执行, 这里指的就是method1()方法有异常
            //System.out.println("这里的代码不会在执行了...");
        } catch (ArithmeticException e){
            System.out.println("解决异常");
        } finally{
            System.out.println("我是main中finally执行的代码.....");
        }
        System.out.println("main方法执行的最后一个方法....");
    }

    public static void method1(){
        int a = 10;
        int b = 0;
        System.out.println("进入异常的开头....");
        throw new ArithmeticException();
        //System.out.println("这里不能再写程序了....");
    }
}

运行结果: 

程序执行开始的地方...
进入异常的开头....
解决异常
我是main中finally执行的代码.....
main方法执行的最后一个方法....

代码分析: main方法开始执行, 执行开头语句, 然后调用method1()方法, 进入method1()方法, 但执行到throw new ArithmeticException(); 退出执行, 这里注意(throw new ArithmeticException(); 后面不能再继续写程序了, 会报错), 然后跳到main方法, main方法对它进行了解决, main方法的程序正常执行.

3.3、比较

3.3.1、在声明方法时候抛出异常

语法:throws(略)

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

问2:  为什么抛出的异常一定是可检查异常(除了Exception中的RuntimeException及其子类以外,其他的Exception类及其子类)?
        答:RuntimeException与Error可以在任何代码中产生,它们不需要由程序员显示的抛出,一旦出现错误,那么相应的异常会被自动抛出。遇到Error,程序员一般是无能为力的;遇到RuntimeException,那么一定是程序存在逻辑错误,要对程序进行修改;只有可检查异常才是程序员所关心的,程序应该且仅应该抛出或处理可检查异常。而可检查异常是由程序员抛出的,这分为两种情况:客户程序员调用会抛出异常的库函数;客户程序员自己使用throw语句抛出异常。

        注意: 覆盖父类某方法的子类方法不能抛出比父类方法更多的异常,所以,有时设计父类的方法时会声明抛出异常,但实际的实现方法的代码却并不抛出异常,这样做的目的就是为了方便子类方法覆盖父类方法时可以抛出异常。(重写方法抛出的异常一定要比父类方法更加精确, 也就是范围更小, 同样也不能抛出父类没有的异常).

3.3.2、在方法中抛出异常

 语法:throw(略)

问1:  抛出什么异常?
        答:对于一个异常对象,真正有用的信息是异常的对象类型,而异常对象本身毫无意义。比如一个异常对象的类型是ClassCastException,那么这个类名就是唯一有用的信息。所以,在选择抛出什么异常时,最关键的就是选择异常的类名能够明确说明异常情况的类。

异常对象通常有两种构造函数:一种是无参数的构造函数;另一种是带一个字符串的构造函数,这个字符串将作为这个异常对象除了类型名以外的额外说明。

问2:  为什么要使用finally块释放资源?
        答:  finally关键字保证无论程序使用任何方式离开try块,finally中的语句都会被执行。因此,当你需要一个地方来执行在任何情况下都必须执行的代码时,就可以将这些代码放入finally块中。当你的程序中使用了外界资源,如数据库连接,文件等,必须将释放这些资源的代码写入finally块中。

注意: 在finally块中不能抛出异常。JAVA异常处理机制保证无论在任何情况下必须先执行finally块然后再离开try块,因此在try块中发生异常的时候,JAVA虚拟机先转到finally块执行finally块中的代码,finally块执行完毕后,再向外抛出异常。如果在finally块中抛出异常,try块捕捉的异常就不能抛出,外部捕捉到的异常就是finally块中的异常信息,而try块中发生的真正的异常堆栈信息则丢失了。

2. 捕获异常:在方法抛出异常之后,运行时系统将转为寻找合适的异常处理器(exception handler)。潜在的异常处理器是异常发生时依次存留在调用栈中的方法的集合。当异常处理器所能处理的异常类型与方法抛出的异常类型相符时,即为合适 的异常处理器。运行时系统从发生异常的方法开始,依次回查调用栈中的方法,直至找到含有合适异常处理器的方法并执行。当运行时系统遍历调用栈而未找到合适 的异常处理器,则运行时系统终止。同时,意味着Java程序的终止。

3.4、try-catch语句

在Java中,异常通过try-catch语句捕获。其一般语法形式为:

try {  
	// 可能会发生异常的程序代码  
} catch (Type1 id1){  
	// 捕获并处置try抛出的异常类型Type1  
} catch (Type2 id2){  
	 //捕获并处置try抛出的异常类型Type2  
}

关键词try后的一对大括号将一块可能发生异常的代码包起来,称为监控区域

Java方法在运行过程中出现异常,则创建异常对象。将异常抛出监控区域之外,由Java运行时系统试图寻找匹配的catch子句以捕获异常。若有匹配的catch子句,则运行其异常处理代码,try-catch语句结束。

 匹配的原则是如果抛出的异常对象属于catch子句的异常类,或者属于该异常类的子类,则认为生成的异常对象与catch块捕获的异常类型相匹配。

注意:一旦某个catch捕获到匹配的异常类型,将进入异常处理代码。一经处理结束,就意味着整个try-catch语句结束。其他的catch子句不再有匹配和捕获异常类型的机会。

注意: Java通过异常类描述异常类型,对于有多个catch子句的异常程序而言,应该尽量将捕获底层异常类的catch子句放在前面,同时尽量将捕获相对高层的异常类的catch子句放在后面。否则,捕获底层异常类的catch子句将可能会被屏蔽。

例如:RuntimeException异常类包括运行时各种常见的异常,ArithmeticException类和ArrayIndexOutOfBoundsException类都是它的子类。因此,RuntimeException异常类的catch子句应该放在 最后面,否则可能会屏蔽其后的特定异常处理或引起编译错误

3.5、try-catch-finally语句

try-catch语句还可以包括第三部分,就是finally子句。它表示无论是否出现异常,都应当执行的内容。try-catch-finally语句的一般语法形式为:

try {  
	// 可能会发生异常的程序代码  
} catch (Type1 id1){  
	// 捕获并处置try抛出的异常类型Type1  
} catch (Type2 id2){  
	 //捕获并处置try抛出的异常类型Type2  
}finally {  
	// 无论是否发生异常,都将执行的语句块  
} 

try、catch、finally语句块的执行顺序:

     1)当try没有捕获到异常时:try语句块中的语句逐一被执行,程序将跳过catch语句块,执行finally语句块和其后的语句;
     2)当try捕获到异常,catch语句块里没有处理此异常的情况:此异常将会抛给JVM处理,finally语句块里的语句还是会被执行,但finally语句块后的语句不会被执行;
     3)当try捕获到异常,catch语句块里有处理此异常的情况:在try语句块中是按照顺序来执行的,当执行到某一条语句出现异常时,程序将跳到catch语句块,并与catch语句块逐一匹配,找到与之对应的处理程序,其他的catch语句块将不会被执行,而try语句块中,出现异常之后的语句也不会被执行,catch语句块执行完后,执行finally语句块里的语句,最后执行finally语句块后的语句。

3.6、小结

1:  try 块:用于捕获异常。其后可接零个或多个catch块,如果没有catch块,则必须跟一个finally块。
2:  catch 块:用于处理try捕获到的异常。
3:  finally 块:无论是否捕获或处理异常,finally块里的语句都会被执行。当在try块或catch块中遇        到return语句时,finally语句块将在方法返回之前被执行。在以下4种特殊情况下,finally块不会被执行:
     1)在finally语句块中发生了异常。
     2)在前面的代码中用了System.exit()退出程序。
     3)程序所在的线程死亡。
     4)关闭CPU

详细了解try-catch-finallyz中return返回情况,可查看文章《Java:简述try-catch-finally中return返回》。

注意: 

对于错误、运行时异常、可查异常,Java技术所要求的异常处理方式有所不同。

      1. 错误:对于方法运行中可能出现的Error,当运行方法不欲捕捉时,Java允许该方法不做任何抛出声明。因为,大多数Error异常属于永远不能被允许发生的状况,也属于合理的应用程序不该捕捉的异常
      2. 运行时异常:由于运行时异常的不可查性,为了更合理、更容易地实现应用程序,Java规定,运行时异常将由Java运行时系统自动抛出,允许应用程序忽略运行时异常
      3. 可查异常:对于所有的可查异常,Java规定:一个方法必须捕捉,或者声明抛出方法之外。也就是说,当一个方法选择不捕捉可查异常时,它必须声明将抛出异常。

能够捕捉异常的方法,需要提供相符类型的异常处理器。所捕捉的异常,可能是由于自身语句所引发并抛出的异常,也可能是由某个调用的方法或者Java运行时 系统等抛出的异常。也就是说,一个方法所能捕捉的异常,一定是Java代码在某处所抛出的异常。简单地说,异常总是先被抛出,后被捕捉的。

异常抛出:任何Java代码都可以抛出异常,如:自己编写的代码、来自Java开发环境包中代码,或者Java运行时系统。无论是谁,都可以通过Java的throw语句抛出异常。从方法中抛出的任何异常都必须使用throws子句。
        异常捕获:捕捉异常通过try-catch语句或者try-catch-finally语句实现。
        总体来说,Java规定:对于可查异常必须捕捉、或者声明抛出。允许忽略不可查的RuntimeException和Error。

四、Java常见异常

4.1、RuntimeException子类:    

4.2IOException

4.3、其他    

 五、相关的问题

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

答:当Java内置的异常都不能明确的说明异常情况的时候,需要创建自己的异常。

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

答:捕捉并处理知道如何处理的异常,而抛出不知道如何处理的异常。

文章参考与: Java:详解Java中的异常(Error与Exception)_王小二(海阔天空)-CSDN博客

THE END
< <上一篇
下一篇>>