Java异常机制全解析:异常分类、处理方法、自定义异常与异常链。

异常的概述

现实生活中经常会发生各种不正常的现象,比如人在生长过程中会生病,车在行驶过程中会爆胎。

软件在运行过程中出现的这些异常问题,就叫做异常(Exception)。我们写程序时需要对某些异常情况做出合理的处理,安全的退出,方式程序崩溃。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.leiou.unit1;

public class Demo1 {
public static void main(String[] args) {
int[] arr = {1,2,4,1,2,-1,5,7};
int value = getValue(arr, -1);
System.out.println(value);
}

public static int getValue(int[] arr, int index) {
if(index < 0) {
System.out.println("索引不能为负数");
}
if(index >= arr.length) {
System.out.println("索引不能大于数组长度");
}
return arr[index];
}

}

上面的程序中,当index值不合法时,我们应当有一定的处理机制,此外还需要告诉调用者方法执行出现问题了,不能盲目的返回一个随意的值。仅仅使用前面的知识点,无论如何处理,都不能满足开发需求。

此时就可以使用Java的异常机制。

实际开发中,异常从面向对象的角度考虑也是一个类,我们可以向上抽取成一个异常类,这个异常类可以对一些不正常的现象进行描述,并封装成对象。

1
2
3
4
5
6
7
8
9
package com.leiou.unit1;

public class Demo2Exception {
public static void main(String[] args) {
int x = 1/0;
System.out.println(x);
}
}

image-20230521143434738

异常分类

异常体系

Java中定义了很多的异常的类,这些异常类可以对应各种各样可能存在的异常事件,它们的公共父类都是 Throwable,如果内置的异常类不满足需求,还可以自己创建异常类。

Throwable是Java中所有异常和错误的父类。

其中 Exception分为两类:RuntimeException和CheckedException

RuntimeException是在运行时出现的异常,也叫作 运行时异常或者不检查异常 ,编译期也不进行检查

CheckException也叫作 **可检查异常或者编译时异常 **,编译期如果没有对这种异常进行处理,编译不通过

方法名 描述
public [String](mk:@MSITStore:C:\Users\Administrator\Desktop\jdk api 1.8_google.CHM::/java/lang/String.html) getMessage() 返回此throwable的详细消息字符串。
public [String](mk:@MSITStore:C:\Users\Administrator\Desktop\jdk api 1.8_google.CHM::/java/lang/String.html) toString() 返回此 throwable 的简短描述
public void printStackTrace() 打印异常的堆栈的跟踪信息

错误(Error)

Error类是Java所有错误的父类,描述了Java运行时系统内部错误和资源耗尽错误,这种错误一般是无法控制的,很难处理的,一般的开发人员无法处理这个错误,所以在开发过程中我们可以不去理会这种错误。

1
2
3
4
5
6
7
8
package com.leiou.unit1;

public class Demo3Error {
public static void main(String[] args) {
int[] arr = new int[1024*1024*1024];
}
}

异常(Exception)

Exception类是Java中所有异常类的父类,它的子类定义了各种各样可能出现的异常事件。Error是无法处理的错误,但Exception是可以处理的异常,因此在开发中我们应当尽可能去处理这些异常。

异常处理

抛出异常

在编写程序时,我们需要考虑程序运行过程中出现问题的情况,比如调用方法时,方法接收的参数需要进行合法性校验,当参数不合法的时候,就需要告诉调用者参数存在一定的问题,此时就需要使用抛出异常的方式来告诉调用者

抛出异常使用 throw 关键字,用它来抛出一个指定的异常对象

1
throw new 异常类名(参数列表);

代码演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.leiou.unit2;

public class Demo1 {
public static void main(String[] args) {
int[] arr = {1,2,4,1,2,-1,5,7};
int value = getValue(arr, 8);
System.out.println(value);
}

public static int getValue(int[] arr, int index) {
if(index < 0) {
throw new IllegalArgumentException("索引不能为负数");
}
if(index >= arr.length) {
throw new ArrayIndexOutOfBoundsException("索引不能大于数组长度");
}
return arr[index];
}

}

声明异常

将异常声明出来,报告给调用者,让调用者对其进行处理。使用 throws 关键字

1
2
3
修饰符 返回值类型 方法名(参数列表) throws 异常类名1, 异常类名2 {

}

代码演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.leiou.unit2;

public class Demo2 {
public static void main(String[] args) {
int[] arr = {1,2,4,1,2,-1,5,7};
int value = getValue(arr, 8);
System.out.println(value);
}

public static int getValue(int[] arr, int index)
throws IllegalArgumentException, ArrayIndexOutOfBoundsException {
if(index < 0) {
throw new IllegalArgumentException("索引不能为负数");
}
if(index >= arr.length) {
throw new ArrayIndexOutOfBoundsException("索引不能大于数组长度");
}
return arr[index];
}

}

当把异常声明在方法上之后,就是告诉调用者,如果调用这个方法,就需要把这些异常处理一下。

其中,当代码中抛出运行时异常时,方法定义上可以不声明,也可以声明

当代码中抛出编译时异常时,方法声明上必须要进行声明, 或者在当前方法中就要对其进行处理。如果声明,则调用者必须对其进行处理,或者继续向上声明

捕获异常

如果程序出现了异常,自己又解决不了,可以把异常声明出来,报告给调用者,让调用者来处理。

如果出现的异常自己能够解决,就可以不声明异常,而是自己捕获异常

1
2
3
4
5
6
7
8
9
try {
可能或者需要处理异常的代码
}catch(异常类名1 变量名1) {
针对异常1处理的逻辑
}catch(异常类名2 变量名2) {
针对异常2处理的逻辑
}finally {
最终一定会执行的逻辑
}

首先使用 try 关键字,将可能出现异常或者需要处理异常的代码包裹起来,之后在多个 catch 代码块中,针对不同的异常进行不同的处理逻辑,最后,无论是否发生异常,无论try、catch代码是否正常执行,最终都要执行一次 finally 中的代码

其中,try 关键字必须要有, catch 关键字可以有 0-多个,finally 关键字可以有0-1个,任意组合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.leiou.unit2;

public class Demo3 {
public static void main(String[] args) {
int[] arr = {1,2,4,1,2,-1,5,7};
try {
int value = getValueProxy(arr, 8);
System.out.println(value);
}catch (IllegalArgumentException e) {
System.out.println("出现了非法参数异常!!!!");
}catch(ArrayIndexOutOfBoundsException e) {
System.out.println(e.getMessage());
// e.printStackTrace();
}finally {
System.out.println("程序执行完毕");
}
}

public static int getValueProxy(int[] arr, int index)
throws IllegalArgumentException, ArrayIndexOutOfBoundsException {
return getValue(arr, index);
}

public static int getValue(int[] arr, int index)
throws IllegalArgumentException, ArrayIndexOutOfBoundsException {
if(index < 0) {
throw new IllegalArgumentException("索引不能为负数");
}
if(index >= arr.length) {
throw new ArrayIndexOutOfBoundsException("索引不能大于数组长度");
}
return arr[index];
}

}

下面我们抬一个杠:finally关键字中的代码是程序不管是否出现异常都会执行的代码,如果前面已经执行了return语句,finally是否还会执行?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.leiou.unit2;

public class Demo4 {
public static void main(String[] args) {
int[] arr = {1,2,4,1,2,-1,5,7};
int value = getValueProxy(arr, -1);
System.out.println(value);
}

public static int getValueProxy(int[] arr, int index)
throws IllegalArgumentException, ArrayIndexOutOfBoundsException {
try {
return getValue(arr, index);
}finally {
// 如果try、catch中全部都有return了,那么finally中不需要有return
System.out.println("程序执行完毕");
}
}

public static int getValue(int[] arr, int index)
throws IllegalArgumentException, ArrayIndexOutOfBoundsException {
if(index < 0) {
throw new IllegalArgumentException("索引不能为负数");
}
if(index >= arr.length) {
throw new ArrayIndexOutOfBoundsException("索引不能大于数组长度");
}
return arr[index];
}

}

即便程序执行过程中遇到了return语句,finally也必然会执行,当遇到了 System.exit(0),finally中的代码才不会执行

随堂练习

  1. 写一个方法void isTriangle(int a,int b,int c),判断三个参数是否能构成一个三角形, 如果不能则抛出异常IllegalArgumentException,显示异常信息 “a,b,c不能构成三角形”,如果可以构成则显示三角形三个边长,在主方法中得到命令行输入的三个整数, 调用此方法,并捕获异常。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    package com.leiou.unit2.test;

    /**
    * 写一个方法void isTriangle(int a,int b,int c),判断三个参数是否能构成一个三角形, 如果不能则抛出异常IllegalArgumentException,
    * 显示异常信息 “a,b,c不能构成三角形”,如果可以构成则显示三角形三个边长,
    * 在主方法中得到命令行输入的三个整数, 调用此方法,并捕获异常。
    */
    public class Test1 {
    public static void main(String[] args) {
    isTriangle(4,5,6);
    isTriangle(2,3,6);
    }
    public static void isTriangle(int a, int b, int c) {
    if(a + b > c && a + c > b && b + c > a) {
    System.out.println("a="+a+",b="+b+",c="+c+"可以构成三角形");
    }else {
    throw new IllegalArgumentException("a,b,c不能构成三角形");
    }
    }
    }

  2. 编写一个计算N个整数平均值的程序。程序应该提示用户输入N的值,如何必须输入所有N个数。如果用户输入的值是一个负数,则应该抛出一个异常并捕获,提示“N必须是正数或者0”。并提示用户再次输入该数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    package com.leiou.unit2.test;

    import java.util.Scanner;

    /**
    * 编写一个计算N个整数平均值的程序。程序应该提示用户输入N的值,
    * 如何必须输入所有N个数。如果用户输入的值是一个负数,
    * 则应该抛出一个异常并捕获,提示“N必须是正数或者0”。并提示用户再次输入该数
    */
    public class Test2 {
    static Scanner scanner = new Scanner(System.in);
    public static void main(String[] args) {
    System.out.print("请输入整数数量:");
    int n = scanner.nextInt();
    double sum = 0;
    for (int i = 0; i < n; i++) {
    int num;
    while (true) {
    try {
    System.out.print("请输入第" + (i+1) + "个整数:");
    num = getNum();
    break;
    }catch (IllegalArgumentException e) {
    System.out.println(e.getMessage() + ",请重新输入");
    }
    }
    sum += num;
    }
    System.out.println("平均值为:" + (sum / n));
    }

    public static int getNum() throws IllegalArgumentException {
    int num = scanner.nextInt();
    if(num >= 0) {
    return num;
    }else {
    throw new IllegalArgumentException("数值必须为正整数或者0");
    }
    }
    }

自定义异常

为什么要使用自定义异常

异常的作用是用来描述程序运行过程中出现的问题的原因,有的时候内置的异常并不能清楚的描述问题。

思考一个场景:用户登录输入用户名和密码,登录失败有2种情况:用户名不存在和密码错误两种情况。面对这两种情况,不可以直观地提醒是哪种错误原因,因为不法分子可能会根据提示的信息来猜测某个账号在系统中是否存在,从而获取到系统中的用户列表,所以这种场景我们不可以精确地提示错误原因,一般都是提示“用户名或密码错误”,此时对于用户而言并不会过度关注具体的原因,重新输入用户名和密码即可,但是对于开发人员来说,要精确地定位到问题的原因,尽管业务上二者的提示文本是一样的,但是在代码层面上我们要能够准确区分两种场景,因此这种时候就需要两个异常类,两个异常类抛出时提示文本相同。

换句话说,异常文本是给用户看的,而异常类的信息是给开发人员看的。

UserNotFoundException和PasswordFailException两个异常类在java中肯定不存在,此时就需要自定义异常类。

自定义异常语法

自定义异常非常简单,java中所有的异常都是类,所有的异常都继承自 Throwable,但是这个类表示的范围太大了,所以我们不继承这个。我们可以继承 Exception 或者 RuntimeException。一般业务类的异常都继承 RuntimeException

1
2
3
4
5
6
7
8
class 自定义异常名 extends Exception或者RuntimeException {
public 自定义异常名() {

}
public 自定义异常名(String msg) {
super(msg);
}
}

此外,自定义异常中也可以有自己的成员变量和成员方法

自定义异常使用

以上面的登录案例为例

首先自定义两个异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.leiou.exception;

public class UserNotFoundException extends RuntimeException {
public UserNotFoundException() {
}

public UserNotFoundException(String message) {
super(message);
}
}
package com.leiou.exception;

public class PasswordFailException extends RuntimeException{
public PasswordFailException() {
}

public PasswordFailException(String message) {
super(message);
}
}

编写测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.leiou.unit3;

import com.leiou.exception.PasswordFailException;
import com.leiou.exception.UserNotFoundException;

import java.util.Scanner;

public class Demo1Login {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入用户名:");
String username = scanner.next();
System.out.println("请输入密码:");
String password = scanner.next();
try {
login(username, password);
System.out.println("登录成功");
}catch (UserNotFoundException | PasswordFailException e) {
System.out.println(e.getMessage());
e.printStackTrace();
}
}

public static void login(String username, String password) {
if(!username.equals("admin")) {
throw new UserNotFoundException("用户名或密码错误");
}
if(!password.equals("123456")) {
throw new PasswordFailException("用户名或密码错误");
}
}

}

随堂练习

  1. 编写程序接收用户输入分数信息,如果分数在0—100之间,输出成绩。如果成绩不在该范围内,抛出异常信息,提示分数必须在0—100之间。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.leiou.unit3;

import java.util.Scanner;

public class Test1 {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入分数:");
double score = scanner.nextDouble();
if(score < 0 || score > 100) {
throw new ScoreIllegalException("分数必须在0-100之间");
}
System.out.println(score);
}
}

class ScoreIllegalException extends RuntimeException {
public ScoreIllegalException() {
}

public ScoreIllegalException(String message) {
super(message);
}
}

自定义异常类中可以有自己的成员变量,在实际的开发中可能会有几百几千种业务异常场景,此时你不可能定义几千个异常类,就可以只定义一个异常,异常中定义一个code,根据code来区分业务场景。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.leiou.exception;

public class GlobalException extends RuntimeException{
private int code;
public GlobalException() {
this.code = 400;
}

public GlobalException(String message) {
super(message);
this.code = 400;
}

public GlobalException(int code, String message) {
super(message);
this.code = code;
}
}
package com.leiou.unit3;

import com.leiou.exception.GlobalException;

public class Demo2 {
public static void main(String[] args) {
try {
method();
}catch (GlobalException e) {
System.out.println(e.getMessage() + "\n错误代码:" + e.getCode());
}
}

public static void method() {
throw new GlobalException(40001, "系统出现了异常");
}
}

异常补充知识点

方法重写中的异常

子类声明的异常范围不能超过父类的范围

父类方法没有声明异常,则子类重写方法也不能声明异常。

如果父类方法声明了异常,则子类重写方法只能声明该异常或者它的子类异常

注意:以上原则只针对编译时异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.leiou.unit4;

import java.io.FileNotFoundException;
import java.io.IOException;

public class Demo1 {
}
class Parent {
public void method1() {}
public void method2() throws IOException {}
}

class Child extends Parent {
@Override
public void method1() throws FileNotFoundException {
}

@Override
public void method2() throws Exception {
}
}

异常链

异常需要封装,但是仅仅封装是不够的,还需要传递异常。

某些系统可能需要用到一些三方的依赖包,这些依赖包提供的方法中可能会抛出一些自己定义的自定义异常,而这些异常在我们系统中是没有进行处理的,而这种情况下如果出现了异常,呈现给用户的就是一大堆报错信息,而不是错误文本,体验就不是很好。

此时可以使用try…catch捕获这些异常,然后将这些异常做一些处理,包装成我们自定义的异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.leiou.unit4;

import com.leiou.exception.GlobalException;

public class Demo2 {
public static void main(String[] args) {
try {
pay();
}catch (GlobalException e) {
System.out.println(e.getCode() + ":" + e.getMessage());
}
}

public static void pay() {
try {
wxPay();
}catch (PayException e) {
throw new GlobalException(40002, e.getMessage());
}
}

public static void wxPay() throws PayException {
System.out.println("微信支付");
throw new PayException("当前商户没有开通微信支付");
}

}

class PayException extends RuntimeException {
public PayException() {
}

public PayException(String message) {
super(message);
}
}

try…with…resource

系统中被打开的资源,比如文件、socket连接等,都需要手动关闭,否则不会被销毁,久而久之就会出现资源泄露导致生产出现重大的事故

1
2
3
4
5
try(/*需要关闭的资源*/){
// 容易出现异常的代码
}catch(Exception e) {
// 处理异常
}

代码演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package com.leiou.unit4;

public class Demo3 {

public static void main(String[] args) {
try(
Resource resource = new Resource();
Resource2 resource2 = new Resource2()
) {
resource.hello();
resource2.hello();
}catch (Exception e) {
e.printStackTrace();
}
}

/**
* JDK7以前
* @param args
*/
public static void main1(String[] args) {
Resource resource = null;
try {
resource = new Resource();
resource.hello();
}catch (Exception e) {
e.printStackTrace();
}finally {
if(resource != null) {
try {
resource.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}

class Resource implements AutoCloseable {

public void hello() {
System.out.println("HelloWorld");
}

@Override
public void close() throws Exception {
System.out.println("资源被关闭");
}
}
class Resource2 implements AutoCloseable {

public void hello() {
System.out.println("HelloWorld2");
throw new RuntimeException("发生异常2");
}

@Override
public void close() throws Exception {
System.out.println("资源被关闭2");
}
}