MENU

Java-47 异常

October 21, 2023 • Read: 72 • Java阅读设置

异常

1 什么是异常

在Java中,会出现代码无法正常编译或运行的情况。Java中总结的导致这种无法正常编译或运行的情况的原因的集合可以称为可抛出错误。

在Java中所有这些可抛出错误的每个类型都定义为一个对应的类(Java是面向对象编程)。

Java中的Throwable,即java.lang.Throwable,类是Java语言中所有错误和异常的父类。
2023-10-21T04:39:27.png

也就是说,Java中的可抛出错误可分为2大类:

  • 错误Error
  • 异常Exception

错误Error,是指代码理论上完全可以正常运行,但系统无法支持的可抛出错误类型,例如栈溢出、堆溢出、JVM本身出问题等。
Error的产生通常是由于代码本身就是不合理、不正常的代码。
Error是不应该去捕获的可抛出错误类型(能捕获,但不应该去捕获),因为捕获是为了处理。换句话说,捕获的错误类型应该是可以通过对其对应代码中的导致无法继续运行下去的部分代码替换为可以运行下去的代码就能解决的,而Error无法通过这种方式得到处理,因为Error本质上是系统无法支持运行,只能是修改代码本身使得代码是系统可支持的运行的代码。
由于产生Error的代码本身在理论上是完全可运行的,即是可以通过编辑的,因此Error无法在编译时检测出,因此Error属于未受检异常(unchecked exceptions)。
2023-10-21T04:45:13.png

异常Exception,是代码不完全能够正常运行,即某些情况下正常,某些情况下无法继续正常运行的可抛出错误即异常。简单地说,Exception对应的代码存在会出现错误而导致无法运行的代码。例如空指针访问、数组角标越界。
由此也可以知道,异常对应的代码是可以通过针对不正常情况专门处理而使得代码完全能够正常运行。
因此,异常是应该去捕获的错误类型,因为它们能够通过对应的针对性代码被处理掉。
根据是否能通过编译,异常分为编译时异常和运行时异常:

  • 编译时异常:在编译时就暴露出的异常,无法正常编译,属于受检异常(checked exceptions)。
  • 运行时异常:可以正常编译,在运行时才会暴露出的异常,属于非受检异常(unchecked exceptions)。
    2023-10-21T04:50:19.png

在Exception中,RuntimeException就是指运行时异常,其他所有都属于编译时异常:
2023-10-21T04:51:16.png

运行时异常子类:
2023-10-21T04:51:38.png

2 异常处理

异常处理是指通过对可能导致代码终止运行的异常进行处理,企图使得代码能够正常运行完成的方式。

Java中,代码一旦出现异常,会产生对应异常的异常类的对象。
Java中的异常处理就是通过对异常对象进行处理而实现。

Java中有2种异常处理方式(或者说异常处理机制):

  • try-catch-finally
  • throws

1) try-catch-finally异常处理机制
a. 什么是try-catch-finally

  • try-catch-finally的异常处理方式是指,将代码中可能出现异常的代码“封装”到try块中,每个try块都有对应的一个或多个catch块,每个catch块对应于一种异常的处理代码。
  • try-catch-finally异常处理机制中的try-catch结构是用来处理异常的,这种异常处理方式本质上就是当try块中可能出现异常的代码出现异常时,终止try块中出现异常的代码及其之后的代码的运行,转而根据该异常对象,去运行对应异常类型的catch块的处理代码,从而实现代码不会因异常而直接终止运行。
    简单地说,catch中的代码就是try中出现的异常代码的替换代码,以防止出现异常代码(即try块中的代码)后的代码会因为异常的出现导致终止而无法运行的情况。
  • finally结构是可选的,finally结构是用于设置try-catch代码运行结束之后一定会运行的代码。
    换句话说,try-catch中对应的代码不论是否是正常结束运行,在结束后就一定会去执行finally中的代码。
    简单地说,只要设置了finally,其中的代码一定会被执行。
  • 注意,在使用try-catch-finally时,try是必写的,将try单独搭配catchfinally都可以,即可以使用成try-catch结构或try-finally结构,当然最完整的就是try-catch-finally结构。

b. try-catch-finally的使用

  • 使用格式为:

    try {可能出现异常的代码}
    catch (异常类型1 异常类型对象名)---实际上相当于接收异常对象的参数设置
    {异常处理代码1}
    catch (异常类型2 异常类型对象名)
    {异常处理代码2}
    ...
    finally {一定会执行的代码}
    • try: try中的代码就是源代码中可能会发生异常的部分。
    • catch: catch中代码是针对try中可能出现的各种异常进行处理的代码。换句话说,如果try中代码完全正常,catch设置的代码一定不会执行。
      try中代码出现异常时,如果没有对应异常类型的catch,则代码会直接终止(没有设置finally时),相当于没有进行异常处理。
    • finally: finally中的代码是无论如何都会被执行的代码,其执行时机就是try-catch代码结束之后,不论是正常结束还是非正常终止都会执行。

c. try-catch-finally的使用注意点

  • 设置try-catch结构的快捷键:选中要try的代码,右键选择Surround with,再选择对应结构即可:
    2023-10-23T04:08:08.png
  • try中的代码是可能(由于异常出现终止)不运行的代码,因此try中定义的变量无法在try外使用,try中的任何操作在try外无法看成是存在的操作。
    也就是说,对于try中的代码的运行效果,要结合catch代码来看,即要综合try中代码正常和不正常运行的效果。
    例如,变量num在try中才被赋值(该操作在会出现异常的代码之后或者本身就是会出现异常的代码),这时在try外无法将num看成是赋值的变量来使用,因为try对应的代码有两种运行情况,要综合这两种运行情况:1)如果try中代码正常运行,那么num就会正常被赋值 2)如果try中代码出现异常,那么try中num赋值对应的代码不会运行,而会转而去运行对应的catch代码。因此这时要保证运行到try-catch-finally结构后的代码时num是被赋值的,要么在try之前给num赋值,要么在所有对应catch中给num赋值。
    2023-10-23T03:34:10.png
  • try中代码出现异常时,try中抛出异常的代码及其之后的代码会被替换为对应catch代码运行,如果没有对应catch,这时如果设置了finally,则运行完finally代码后终止运行,如果没有设置finally,则直接终止运行。
    2023-10-23T03:37:05.png
  • catch中异常处理代码通常会设置为以下2种:

    • e.getMessage():其中e为异常类对应的对象,该方法返回一个与该异常类型对应的String
      2023-10-23T03:41:14.png
    • e.printStackTrace():该方法无返回值,会打印该异常类型对应的String及异常回溯跟踪记录。
      2023-10-23T03:42:03.png

也就是说,异常处理 通常 是指在异常发生时仅终止出现异常的代码并给出一个源代码发生异常的信息,然后继续运行其后续代码。

  • 每个try可以对应多个catch,一旦try中出现异常就会根据该异常去匹配catch,一旦匹配成功,运行对应异常处理代码后就跳出try-catch结构了。也就是说,一次异常情况发生,最多只会运行一个异常处理代码(没有匹配到,则不会运行异常处理代码)。
    2023-10-23T03:56:30.png
    因此,设置多个catch时,声明在前面的catch对应的异常类型不能是声明在后面的catch对应的异常类型的父类,要么是子类要么无关,一方面如果不这样后面的catch永远不会运行,就没有意义,另一方面会报错。
    2023-10-23T03:55:52.png

    catch的设置的类型和个数,主要取决于每个异常类型的处理方式,如果所有异常类型处理方式都一样,那就只需要设置一个对应所有异常类型的catch即可,即Exception类型,因此它是所有异常的父类。如果不同异常类型处理方式不同,就需要对每个异常类型设置一个对应的catch

  • 对可能出现异常的代码,使用try-catch-finally机制,不代表就不会出现异常,因为异常处理代码本身也可能出现异常。
    对于编译时异常,异常处理是必要的,它通过保证会出现编译时异常的代码出现异常时用catch代码来替换异常代码来运行,保证了代码即使“编译时异常”也会运行不会出现编译时异常的代码,从而使得代码能够正常编译。因此,对于可能会出现编译时异常的代码,会要求必须将所有可能出现的编译时异常进行异常处理后,才能正常编译。但运行时异常不会有报错来提示进行异常处理。
    因此如果异常处理代码本身存在编译时异常,一定会报错提示需要对异常处理代码也进行异常处理,但也可能存在运行时异常的情况,如果知道会出现运行时异常,也可以对异常处理代码本身进行异常处理。
    因此try-catch-finally是可以嵌套的。
    2023-10-23T04:05:49.png
    2023-10-23T04:10:09.png
    2023-10-23T04:10:56.png
  • finally的使用场景:像数据库、输入输出流、网络编程Socket等资源,JVM是不能自动回收的,需要手动进行资源的释放。这种资源的释放,往往对应的代码是会出现异常的代码,即放在try中的,因此这时资源的释放操作就需要声明在finally中。

2) throws异常处理机制
a. throws异常处理机制

  • try-catch-finally异常处理机制是直接对可能出现异常的代码进行处理。throws异常处理机制是针对一个方法的整块代码,将其声明为一个可能出现异常的整体,当该方法被使用时,作为一个可能出现异常的整体(代码)被使用,此时使用该方法的方法其中的调用代码相当于一个存在异常的代码,因此该方法要么也将自己声明为一个可能出现异常的整体,要么使用try-catch-finally异常处理机制对自己代码中可能存在异常的代码(包括对声明为异常方法的调用对应的调用代码)进行异常处理。
  • throws异常处理机制本质上并没有对可能出现异常的代码进行处理,而是通过声明可能出现异常的代码所在的方法为一个可能出现异常的方法,使可能出现异常的代码可以作为一个可能出现异常的方法来被间接使用,使用该方法的代码就要承担使用可能出现异常的代码的后果,即要么异常处理,要么也作为异常方法,要么调用异常方法时出现异常终止。
  • 形象地说,throws异常处理机制是将自己本身有问题要被处理,通过将自己作为使用自己的方法的问题,转换为让使用自己的方法来处理自己。

b. throws异常处理机制的使用

  • 格式:在可能出现异常的代码所在的方法的方法定义之后、方法体之前使用关键字throws,后接异常类型。异常类型可以有多个,用逗号分隔,代表声明的该方法可能存在的异常:

    权限修饰符 [关键字] 返回值类型 方法名(参数) throws 异常类型1, 异常类型2, ... 
    {
    }
  • 注意点:

    • 对于存在编译时异常的代码所在的方法,使用throws异常处理机制时,要求将其所在方法throws的异常类型一定要体现出其存在的所有编译时异常,否则会报错。
      2023-10-23T08:48:10.png
      2023-10-23T08:48:27.png
    • 对于存在运行时异常的代码所在的方法,使用throws异常处理机制时其所在方法throws的异常类型就没有要求。
      可以这样理解:throws的异常类型是声明该方法中可能存在的异常类型,这样其他方法使用该异常方法时,对应的调用代码就相当于可能存在这些声明的异常的代码。对于编译时异常,要求一定要throws出,这样使用声明为存在编译时异常的异常方法,对应的调用代码就相当于存在对应编译时异常的代码,因此如果要异常处理,一定要处理这些编译时异常,这对于异常代码的正常编译来说不矛盾。而对于运行时异常,即使throws的异常不准确也没关系,因为这也只是表明使用该方法的方法中对应的调用代码相当于存在这个运行时异常的代码,但运行时异常本身就没有强制要求被处理,因此这时如果要异常处理运行时异常,完全可以按照与throws出的异常类型完全不同的来处理。
      2023-10-23T08:56:54.png
    • throws异常处理机制的效果是:

      • 作为异常方法的方法:在出现异常时,其异常代码和后续代码不运行,直接终止运行。
      • 使用异常方法的方法:1)如果没有异常处理,则使用异常方法对应的调用代码及其后续代码不运行,即直接终止运行。2)如果进行了异常处理,则遵循try-catch-finally异常处理机制。
    • 子类重写父类方法时,子类throws的异常类型与父类throws的异常类型有以下要求:

      • 对于运行时异常,没有要求。但不能是父类throws运行时异常,子类重写时throws编译时异常。
        并且如果父类对应方法没有throws异常,那么子类throws运行时异常也没有任何要求。
        2023-10-24T02:31:12.png
        2023-10-24T02:31:29.png
        2023-10-24T02:41:52.png
      • 对于编译时异常,父类throws的异常类型要么与子类throws的异常类型相同,要么也形成子父类关系,要么子类重写时不throws异常。并且如果父类没有throws编译时异常,子类也不能throws编译时异常。
        理解:因为编译时异常是一定要被处理的异常。由于多态性,存在通过对父类的方法的异常处理,实际使用子类对应的重写方法的情况,因此子类重写父类的方法时throws的异常类型要能够使用针对父类的该方法的异常处理。
        2023-10-24T02:35:45.png
        2023-10-24T02:36:45.png
        2023-10-24T02:43:01.png

3) 两种异常处理机制的使用选择

  • 如果父类对应方法没有throws异常,子类重写父类对应方法时如果存在编译时异常,也不能throws异常,这时只能使用try-catch-finally异常处理机制来处理编译时异常。
  • 执行的方法中,先后调用了另外几个方法,这几个方法是递进关系执行的,此时建议对其中每个方法使用throws进行异常处理,而在执行方法中使用try-catch-finally处理机制对这些方法进行整体异常处理,因为这些方法的使用相当于是整体使用,如果前面的方法出现异常,后面的方法基于前面的方法的结果因此必然也会出现异常,因此,如果在每个方法中进行try-catch-finally异常处理机制就显得冗余。

3 手动选择抛出异常

1) 什么是手动选择抛出异常
当代码出现异常时,系统会自动根据对应异常类型生成对应的异常对象抛出,并且会终止代码的继续运行。

换句话说,代码出现异常的标志是有对应异常类型的对象生成,而系统针对出现异常的代码会有专门的操作:a. 抛出异常对象,如果有对应异常处理代码,则进行对应的异常处理;b. 终止当前出现异常的代码的继续运行。

手动选择抛出异常是指,我们可以自定义地在代码中生成异常对象,这意味着我们能够自定义代码在哪些情况下产生异常。

2) 如何手动选择抛出异常
使用关键字throw,后接对应异常类型的对象即可,格式为:throw new 异常类型对应的构造器

3) 手动选择抛出异常的注意点
如果代码真实地存在某些异常,当异常出现时,系统是会自动抛出对应异常对象的。因此,手动选择抛出异常往往是针对那些本并不存在的异常,但我们希望作为异常情况对待的代码。比如,赋值不合理(不符合原设计)的情况下,对于系统是属于正常赋值操作,但我们希望不符合设计要求的赋值操作作为一种异常,也就是不希望不符合设计要求的赋值发生后,其后续代码还能运行。

因此,手动选择抛出异常时,抛出的异常对象通常只会是2种:

  • Exception:考虑作为存在编译时异常的代码看待
  • RuntimeException:考虑作为存在运行时异常的代码看待

4) 示例

  • 符合设计的赋值,会正常运行所有代码
    2023-10-24T08:26:55.png
  • 不符合设计的赋值,会作为异常看待,出现异常时会终止后续代码运行
    2023-10-24T08:27:44.png

    • 作为编译时异常时,要求必须处理
      2023-10-24T08:28:36.png
      2023-10-24T08:29:10.png

4 自定义异常类

1) 什么是自定义异常类
自定义异常类是指,自己定义一个异常的类型。

2) 如何自定义异常类
step1: 继承Java中的异常类,比如ExceptionRuntimeException

既然是异常类,那么其对应的对象就是异常对象,即生成这个类的对象的代码要被作为异常的代码对待。

只有属于Exception的类才会被作为异常类(RuntimeException本质上也属于Exception),因此自定义异常类必须作为Java某异常类的子类。
当然,继承Exception或编译时异常类就作为编译时异常类。继承运行时异常类就作为运行时异常类。

step2: 定义全局常量serialVersionUID,每个异常类都会有自己的这个全局常量,这是用于标识异常类的,后续学习后会更理解。
2023-10-24T08:57:08.png

step3: 定义构造器。
定义构造器时要注意对继承父类的一些属性进行赋值,因为异常类的很多共有方法,比如getMessage(),是基于父类中定义的属性的。

3) 示例

  • 自定义一个运行时异常类
    2023-10-24T08:58:48.png
    2023-10-24T08:59:08.png
  • 自定义一个编译时异常类
    2023-10-24T08:59:59.png
    2023-10-24T09:00:33.png
    2023-10-24T09:00:53.png
Last Modified: October 25, 2023