# Java教程 - 10 异常

# 10.1 异常的概念

什么是异常?

异常(Exceptions)是指在程序执行过程中出现的错误或异常情况,导致程序无法继续正常执行的事件。

但是程序语法错误和逻辑错误不属于异常。在 Java 中,异常分为 Error 和 Exception,但是 Error 是 Java 虚拟机无法解决的严重问题,例如递归调用没有跳出条件,导致无限递归产生的 StackOverflowError,或内存不足造成的 OutOfMemeryError,Error 异常一般不编写针对性的代码来处理,只能改代码来处理。我们在这里要针对性处理的是 Exception 异常。

Exception 又分为 编译时异常运行时异常

  • 编译时异常:在编译的时候就需要对可能存在的异常进行处理,也叫受检异常。
  • 运行时异常:在编译的时候不强制要求处理,也叫非受检异常,java.lang.RuntimeException 类及它的子类都是运行时异常。

异常类的继承关系及常见的异常类:

# 1 运行时异常举例

例如:整数除数是0会抛出异常。

public static void main(String[] args) {
    int i = 5 / 0;
    System.out.println(i);
}
1
2
3
4

执行的时候就会发生错误:

Exception in thread "main" java.lang.ArithmeticException: / by zero
	at com.doubibiji.Test.main(Test.java:8)
1
2

当程序出现错误的时候,我们通常称之为抛出异常,抛出异常后,程序终止执行。

# 2 编译时异常举例

以 IO 异常为例:

public static void main(String[] args) {
    FileInputStream fis = new FileInputStream("xxx.txt");
    int x = fis.read();
    System.out.println(x);
    fis.close();
}
1
2
3
4
5
6

上面的代码无法通过编译,因为没有对编译时异常进行处理。

上面的 FileInputStream 是用来读取文件内容的,在后面的文件与IO章节会讲到。

# 10.2 捕获异常

程序抛出异常就无法继续执行了,但是任何程序都不可能是完美的没有bug的,只能尽可能的对错误进行预防和处理。

对异常进行预防和提前处理,这种行为通常称之为异常捕获。

一个程序出现任何错误,就停止运行肯定不是我们希望看到的,即使程序出现错误,哪怕给与用户一个错误提示,也是一种积极的处理方式。

# 1 异常捕获语法

最简单的异常捕获语法:

try {
  // 可能会抛出异常的代码
} catch (Exception e) {
  // 捕获并处理异常
}
1
2
3
4
5

举个栗子,捕获前面例子中运行时异常:

public static void main(String[] args) {
    try {
        int i = 5 / 0;
        System.out.println(i);
    } catch (Exception e) {
        // 捕获并处理异常
        System.out.println("除数不能为0");
    }

    System.out.println("异常后的代码");
}
1
2
3
4
5
6
7
8
9
10
11

将可能抛出异常的代码,放在 try 代码块中,上面的程序执行到 int i = 5 / 0; 会抛出异常,然后执行 catch 块中的语句。

出现异常后,在 try 代码块中,抛出异常语句之后的代码是无法被继续执行的,但是在 try-catch 后面的代码可以继续执行。

执行结果:

除数不能为0 异常后的代码

这样在发生错误的时候,可以给用户一个提示。当然具体的处理方式需要根据业务需求的处理,这里只是举个例子。

另外如果方法有返回值,还需要在 catch 块中,根据需要进行返回:

public static int test() {
    try {
        int[] nums = new int[]{1, 2, 3};
        return nums[3];         // 会抛出异常
    } catch (Exception e) {
        e.printStackTrace();
        return 0;		// 需要返回值,否则方法没有返回值
    }
}
1
2
3
4
5
6
7
8
9

# 2 打印异常详细信息

捕获异常后,在 catch 块中,可以通过异常对象和 stacktrace 对象打印异常的详细信息。

public static void main(String[] args) {
    try {
        int i = 5 / 0;
        System.out.println(i);
    } catch (Exception e) {
        // 捕获并处理异常
        System.out.println("除数不能为0");
        System.out.println(e.getMessage()); // 打印异常信息:/ by zero
        e.printStackTrace();    // 打印异常堆栈跟踪信息
    }

    System.out.println("异常后的代码");
}
1
2
3
4
5
6
7
8
9
10
11
12
13

执行结果:

# 3 捕获指定类型的异常

在程序运行时,可能会出现不同类型的异常,有时候可能只想捕获和处理指定类型的异常,这个时候就需要根据类型来捕获异常了。因为 Exception e 会捕获所有类型的异常。

语法:

public static void main(String[] args) {
    try {
        int i = 5 / 0;
        System.out.println(i);
    } catch (ArithmeticException e) {
        // 捕获并处理异常
        System.out.println("除数不能为0");
        e.printStackTrace();
    }
  
    System.out.println("异常后的代码");
}
1
2
3
4
5
6
7
8
9
10
11
12

上面捕获了 ArithmeticException 类型的异常,在捕获异常后,打印了异常信息和提示信息。

需要注意,上面只是捕获了 ArithmeticException 类型的异常,如果有代码抛出了其他类型的异常,是无法捕获的。


举个栗子,我们修改代码如下:

public static void main(String[] args) {
    try {

        int[] nums = new int[3];
        System.out.println(nums[3]);

    } catch (ArithmeticException e) {
        // 捕获并处理异常
        System.out.println("除数不能为0");
        e.printStackTrace();
    }
  
    System.out.println("异常后的代码");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

在上面的代码中,nums 的长度为 3,所以 nums[3] 会报数组越界异常(ArrayIndexOutOfBoundsException) ,但是我们没有捕获这个异常,导致程序终止。

执行信息:

Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
	at com.doubibiji.Test.main(Test.java:11)
1
2

如果想单独针对某个异常做特殊处理,并其他异常使用 Exception 进行兜底,这种情况需要捕获多个异常。

# 4 捕获多个异常

我们可以同时捕获多个异常,并对每种异常可以采用不同的处理。

举个栗子:

public static void main(String[] args) {
    try {
        int[] nums = new int[3];
        System.out.println(nums[3]);
    } catch (ArithmeticException e) {
        // 捕获并处理异常
        System.out.println("除数不能为0");
        e.printStackTrace();
    } catch (ArrayIndexOutOfBoundsException e) {
        // 捕获并处理异常
        System.out.println("数组越界");
        e.printStackTrace();
    } catch (Exception e) {
        e.printStackTrace();
    }

    System.out.println("异常后的代码");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

如果我们不能判断是否还会抛出其他类型的错误,可以在最后添加 catch (Exception e) 用来捕获其他类型的错误。

# 5 对多个异常使用相同的处理

刚才针对指定的异常进行捕获,然后针对性的处理。

如果相对多个异常采用相同的处理,那么可以使用 | 操作符。

举个例子:

try {
    int[] nums = new int[3];
    System.out.println(nums[3]);
} catch (ArithmeticException | ArrayIndexOutOfBoundsException e) {	// 统一处理两个类型的异常
    // 捕获并处理异常
    System.out.println("统一处理多个异常");
    e.printStackTrace();
} catch (Exception e) {
    e.printStackTrace();
}
1
2
3
4
5
6
7
8
9
10

# 6 异常finally

finally 表示的是无论是否发生异常都要执行的代码。

举个栗子:

public static void main(String[] args) {
    try {
        int[] nums = new int[3];
        System.out.println(nums[3]);
    } catch (Exception e) {
        System.out.println("出现异常");
        return ;
    } finally {
        System.out.println("出现异常也会被执行");
    }

    System.out.println("异常后的代码");
}
1
2
3
4
5
6
7
8
9
10
11
12
13

执行结果:

出现异常
出现异常也会被执行
1
2

哪怕出现异常,使用 return 返回,终止了方法的执行,但是 finally 代码块还是在返回之前被执行。

finally 一般在文件读写操作的时候用的比较多,就是不管是否发生错误都要关闭文件,所以可以将文件的关闭操作放在 finally 块中。

举个例子,捕获前面例子中的编译时异常:

public static void main(String[] args) {
    FileInputStream fis = null;
    try {
        fis = new FileInputStream("xxx.txt");
        int x = fis.read();
        System.out.println(x);
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            fis.close();    // 关闭也会抛出异常
        } catch(IOException e) {
            e.printStackTrace();
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

在上面的代码中,对文件进行读取,最终在 finally 块中关闭流,关于 IO 后面的章节再讲,这里可以看出,try-catch-finally 是可以嵌套的。


再看一段代码:
public class ExceptionTest {
    public static void main(String[] args) {
        int i = test();
        System.out.println(i);  // 3
    }

    public static int test() {
        try {
            int[] nums = new int[]{1, 2, 3};
            return nums[3];         // 抛出异常
        } catch (Exception e) {
            e.printStackTrace();
            return 2;
        } finally {
            System.out.println("一定会执行");
            return 3;
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

在 test() 方法中,try 代码块中会抛出异常,进入到 catch 代码块中,但是在 return 2 之前会执行 finally 代码块中的内容,所以会先执行 return 3,但是 return 后方法就结束了,所以返回值是 3

# 10.3 throws异常

前面使用 try-catch-finally 捕获异常并进行处理,这是一种异常的处理方式。

另一种异常的处理方式就是使用 throws 进行处理,其实这种处理方式就是不处理,而是将异常往上抛,抛给调用的方法,并没有真正处理异常。

例如之前的编译时异常,我们使用了 try-catch-finally 进行捕获处理,也可以使用 throws 抛出异常。

public class ExceptionTest {
    public static void main(String[] args) {
        try {
            test();
        } catch(IOException e) {
            e.printStackTrace();
        }
    }

  	// 将IOException抛出,交由调用者处理
    public static void test() throws IOException {
        FileInputStream fis = new FileInputStream("xxx.txt");
        int x = fis.read();
        System.out.println(x);
        fis.close();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

在上面的代码中,test() 中的代码是会抛出编译时异常的,但是没有使用 try-catch-finally 进行捕获处理,而是直接使用 throws 指明此方法会抛出的异常,那么此方法就不用对异进行处理了,而是由调用者进行处理,调用者可以使用 try-catch-finally 进行捕获处理,或者也可以使用 throws 进行处理,继续向上抛出。

在子类重写父类方法的时候,父类方法使用throws指明了抛出的异常类型,那么子类方法throws的异常类型不能大于父类抛出的异常类型,只能是父类抛出异常的类型或其子类型。如果父类方法没有throws异常,重写的时候也不能throws异常。

# 10.4 异常的传递

什么是异常的传递?

异常的传递,就是当方法执行的时候出现异常,如果没有进行捕获,就会将异常传递给该方法的调用者,如果调用者仍未处理异常,则继续向上传递,直到传递给主程序,如果主程序仍然没有进行异常处理,则程序将被终止。

举个栗子:

public class Test{
    public static void main(String[] args) {
        try {
            testException();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void testException() {
        testNullPoint();
    }

    public static void testNullPoint() {
        String str = null;
        str.length();   // 会抛出空指针异常
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

上面代码,执行 testNullPoint() 会抛出空指针异常,因为 testNullPoint() 函数中没有进行异常处理,则异常会抛给 testException() 函数,testException() 没有进行异常处理,则抛给 main() 函数,main() 函数中则对异常进行了处理,程序不会崩溃。

利用异常的传递性,如果我们在 main() 函数中进行了异常捕获,无论程序哪里发生了错误,最终都会被传递到 main() 函数中,保证所有的异常都会被捕获。但是不要所有的异常都在 main()函数中处理,main() 函数中的异常处理只是兜底处理。

# 10.5 主动抛出异常

除了代码执行的时候出错,系统会主动抛出异常,我们还可以根据实际的业务需要,主动抛出异常。

首先创建一个异常对象,然后使用 throw 关键字抛出异常对象(不是 throws)。

举个栗子:

public class Test{
    public static void main(String[] args) {
        try {
            String name = inputUsername();
            System.out.println("输入的用户名:" + name);
        } catch (Exception e) {
            System.out.println("用户名格式错误,请重新输入");
        }
    }

    /**
     * 从键盘读取用户名
     */
    public static String inputUsername() {
        System.out.println("请输入用户名:");

        Scanner scanner = new Scanner(System.in);
        // 读取键盘输入
        String name = scanner.nextLine();

        if (null == name || name.length() > 16 || name.length() < 8) {
            throw new RuntimeException("用户名格式错误"); // 主动抛出异常
        }

        return name;
    }
}
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

在上面的代码中,如果输入用户名不正确,主动创建一个 RuntimeException 异常对象,并使用 throw 抛出。

当输入abc的时候,执行结果:

请输入用户名:
abc
用户名格式错误,请重新输入
1
2
3

# 10.6 自定义异常类

除了使用 Java 内置的异常类型外,您还可以自定义异常类,以便更好地描述特定的异常情况。

自定义异常类一般情况下都是继承 Exception 或 RuntimeException 类,如果继承 Exception,属于编译时异常,需要在编译的时候就要处理,如果继承 RuntimeException,那么是运行时异常。

异常类建议以 Exception 结尾,见名知意。

举个栗子:

下面我们首先自定义了两个异常类,然后根据需要抛出这两个异常类的对象,在捕获异常的时候,根据异常类型进行不同的处理。

并在自定义异常类中可以封装自定义的参数。

import java.util.Scanner;

// 定义异常类一:一般自定义异常类,定义两个构造方法,并调用父类的构造方法
class UsernameEmptyException extends RuntimeException {
    public UsernameEmptyException() {}
    public UsernameEmptyException(String message) {
        super(message);
    }
}

// 定义异常类二:可以在自己的异常类中定义各种参数
class UsernameLengthException extends RuntimeException {
    int minLength;
    int maxLength;
    String message;

    // 自定义异常类
    public UsernameLengthException(String message, int minLength, int maxLength) {
        super(message);		// 调用父类构造,并传递异常信息
        this.message = message;
        this.minLength = minLength;
        this.maxLength = maxLength;
    }

    @Override
    public String toString() {
        return this.message + ", minLength:" + this.minLength + ", maxLength:" + this.maxLength;
    }
}


/**
 * 测试类
 */
public class ExceptionTest {

    public static void main(String[] args) {
        try {
            String username = inputUsername();
            System.out.println("输入的用户名:$password");
        } catch(UsernameEmptyException e) {
            System.out.println("用户名不能为空");
        } catch (UsernameLengthException e) {
            System.out.println(e.getMessage());
        }
    }

    public static String inputUsername() {
        System.out.println("请输入用户名:");
        Scanner scanner = new Scanner(System.in);
        // 读取键盘输入
        String name = scanner.nextLine();

        if (null == name || name.length() < 1) {
            throw new UsernameEmptyException("用户名为空"); // 抛出异常
        }

        if (name.length() > 16 || name.length() < 8) {
            throw new UsernameLengthException("用户名格式错误", 8, 16); // 抛出异常
        }

        return name;
    }
}
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
64