导学

本节课程,我们将会学习到异常,以及如何处理可能会发生的异常。
首先,什么是异常?异常可以理解为意外,例外的意思,本质上是程序出现的错误。错误在我们编写程序的过程中经常会发生,包括编译期间和运行期间的错误。比如括号没有正常的配对,语句少写了分号,关键字编写错误等就是编译期间会出现的错误。通常这些编译错误编译器会帮助我们进行修订。
那么,运行期间的错误,我们也曾遇到过。比如使用空的对象引用调用方法、数组访问时下标越界、算数运算除数为0、类型转换时无法正常转型等,这些错误在编译的时候完全没有提示。

在程序运行过程中,意外发生的情况,背离我们程序本身的意图的表现,都可以理解为异常。

那么,为什么会出现异常呢?

用户不正当的输入 本身系统代码编写的问题

如果不对异常进行处理,那么轻则数据错误、丢失,重则程序崩溃。比如下面这段代码

  1. public class ExceptionStudy {
  2. public static void main(String[] args) {
  3. System.out.println("******程序开始运行*******");
  4. System.out.println("******数学计算:"+ (10/2) +"*******");
  5. System.out.println("******程序运行结束*******");
  6. }
  7. }
  8. 运行结果:
  9. ******程序开始运行*******
  10. ******数学计算:5*******
  11. ******程序运行结束*******
  12. public class ExceptionStudy {
  13. public static void main(String[] args) {
  14. System.out.println("******程序开始运行*******");
  15. System.out.println("******数学计算:"+ (10/0) +"*******");
  16. System.out.println("******程序运行结束*******");
  17. }
  18. }
  19. 运行结果:
  20. ******程序开始运行*******
  21. Exception in thread "main" java.lang.ArithmeticException: / by zero
  22. at com.ntdodoke.preparation.usuallyClass.ExceptionStudy.main(ExceptionStudy.java:6)

在出现异常的地方,程序就会被「异常指令流」直接终止,不再往下运行。为了让程序在出现异常后依然可以正常执行,所以我们必须正确处理异常来完善代码的编写。
在Java中,提供了一种强大的异常处理机制来帮助我们实现异常的处理

异常介绍

1. Java异常 - 图1

Error是程序无法处理的错误,表示运行应用程序中较为严重的问题,是代码运行时Java虚拟机中出现的问题。这种错误是不可查的,它们在应用程序的控制和处理能力之外,而且绝大多数是程序运行时不允许出现的状况。 对于设计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。 Excepetion是程序本身可以处理的异常。异常处理通常针对这种类型异常的作出处理 。RuntimeException是非检查异常,也就是编译器不要求强制处理的异常。程序员可以针对这些异常进行捕获或放任不管。检查异常包括IO异常和SQL异常,编译器要求必须在代码中处理这些异常。

异常处理

针对于异常处理,通常我们最简单的方式就是思考全面,检查代码。将可能会发生的错误预先考虑透彻。那么也有可能会出现一些无法预料到的情况,我们就可以使用到Java的异常处理机制。在Java中异常处理机制分为两种:抛出异常和捕获异常。
抛出异常指的是一个方法中出现错误引发的异常时,方法会创建异常对象,交给运行时系统进行处理。在异常对象中包含异常类型,异常出现时的程序状态等。运行时系统捕获到这个异常后,进入捕获异常环节,运行时系统会找合适的处理器,与抛出异常匹配后进行处理,如果没找到则程序终止。Java规定,对于检查异常必须捕获、或者声明抛出。对于非检查异常(RuntimeException及其子类)和Error及其子类,允许忽略。
抛出异常、捕获异常通过5个关键字实现:try、catch、finally、throw、throws。try、catch、finally通常用来捕获异常,throws通常用来声明异常,throw通常用来抛出异常。
1. Java异常 - 图2

try…catch…finally

语法:

  1. try {
  2. //用于捕获异常
  3. 可能会发生异常的代码块;
  4. } catch(Exception e) {//异常类型
  5. //用于处理try中捕获的异常
  6. } finally {
  7. //无论是否发生异常都会执行的代码
  8. }

try块后可接零个或多个catch块,如果没有catch块,则必须跟一个finally块。 也就是说try必须和catch,finally组合使用。catch、finally也不允许单独存在。

image.png
案例:

  1. public class TryDemo {
  2. public static void main(String[] args) {
  3. //定义两个整数,输出两个数的商
  4. Scanner input = new Scanner(System.in);
  5. System.out.println("======运算开始=======");
  6. try{
  7. System.out.println("请输入第一个整数");
  8. int one = input.nextInt();
  9. System.out.println("请输入第二个整数");
  10. int two = input.nextInt();
  11. System.out.println("one和two的商是:" + one / two);
  12. }catch(Exception e) {//这是对异常种类的猜测,通常可以采用异常的父类,就不用猜测具体是哪个子类异常了
  13. e.printStackTrace();//在控制台打印出异常种类,错误信息和出错位置等,输出位置比较随机
  14. System.out.println("程序出错了~~~");
  15. } finally {
  16. System.out.println("运算结束");
  17. }
  18. }
  19. }

在Java中,可以使用多重catch语句块,针对于同一块代码可能发生的不同种类异常作出处理。比如上述代码可能会发生算术异常也可能会发生其他异常。但是两个catch块中对于异常的猜测是不允许相同的,而且对于父类的异常是必须要放置在子类的异常catch代码块下面的。兄弟异常的catch代码块位置可以随意。

  1. public class TryDemo {
  2. public static void main(String[] args) {
  3. //定义两个整数,输出两个数的商
  4. Scanner input = new Scanner(System.in);
  5. System.out.println("======运算开始=======");
  6. try{
  7. System.out.println("请输入第一个整数");
  8. int one = input.nextInt();
  9. System.out.println("请输入第二个整数");
  10. int two = input.nextInt();
  11. System.out.println("one和two的商是:" + one / two);
  12. } catch(InputMismatchException a) {//输入格式异常
  13. System.out.println("请输入整数!");
  14. a.printStackTrace();
  15. } catch(ArithmeticException b) {//数学运算异常
  16. System.out.println("除数不能为0!");
  17. b.printStackTrace();
  18. } catch(Exception e) {//这是对异常种类的猜测,通常可以采用异常的父类,就不用猜测具体是哪个子类异常了
  19. e.printStackTrace();//在控制台打印出异常种类,错误信息和出错位置等,输出位置比较随机
  20. System.out.println("程序出错了~~~");
  21. } finally {
  22. System.out.println("运算结束");
  23. }
  24. }
  25. }

我们会发现,最终输出的异常信息,是我们猜测的某一个异常。同样我们不可能猜测出所有的异常信息,所以需要在catch代码块的最下面写出一个父类异常catch代码块,以免发生遗漏!

终止finally执行的方法

在之前的课程中,我们可以看到,finally语句块中的内容无论怎样都会运行,那么有没有一种方法可以终止finally中方法的运行呢

  1. public class TryDemo {
  2. public static void main(String[] args) {
  3. //定义两个整数,输出两个数的商
  4. Scanner input = new Scanner(System.in);
  5. System.out.println("======运算开始=======");
  6. try{
  7. System.out.println("请输入第一个整数");
  8. int one = input.nextInt();
  9. System.out.println("请输入第二个整数");
  10. int two = input.nextInt();
  11. System.out.println("one和two的商是:" + one / two);
  12. } catch(InputMismatchException a) {//输入格式异常
  13. System.exit(1);//终止程序运行
  14. System.out.println("请输入整数!");
  15. a.printStackTrace();
  16. } catch(ArithmeticException b) {//数学运算异常
  17. System.out.println("除数不能为0!");
  18. b.printStackTrace();
  19. } catch(Exception e) {//这是对异常种类的猜测,通常可以采用异常的父类,就不用猜测具体是哪个子类异常了
  20. e.printStackTrace();//在控制台打印出异常种类,错误信息和出错位置等,输出位置比较随机
  21. System.out.println("程序出错了~~~");
  22. } finally {
  23. System.out.println("运算结束");
  24. }
  25. }
  26. }

System中存在exit方法,该方法的作用可以用来终止Java虚拟机的运行,传入的参数是数字,这个数字只要不是数字0描述的都是异常终止状态。可以使用该方法来使当前程序无条件终止运行。

return关键字在异常处理中的作用

在之前的学习中,我们知道return可以用来提供方法的返回值。那么return是否可以用在异常处理中呢,或者又有什么作用呢

  1. public class TryTest {
  2. public static void main(String[] args) {
  3. int result = test();
  4. System.out.println("运行的结果为:" + result);
  5. }
  6. public static int test() {
  7. Scanner input = new Scanner(System.in);
  8. System.out.println("======运算开始=======");
  9. try{
  10. System.out.println("请输入第一个整数");
  11. int one = input.nextInt();
  12. System.out.println("请输入第二个整数");
  13. int two = input.nextInt();
  14. System.out.println("one和two的商是:" + one / two);
  15. return one / two;
  16. } catch(ArithmeticException b) {
  17. System.out.println("除数不能为0!");
  18. b.printStackTrace();
  19. return 0;
  20. } finally {
  21. System.out.println("运算结束");
  22. return -10000;
  23. }
  24. }
  25. }

当try,catch,finally三个语句块中都存在return关键字时,无论程序运行正常不正常,最终的结果都是-10000;原因在于finally无论如何都会执行。

throw和throws

使用throws声明异常类型

可以通过throws声明将要抛出何种类型的异常,通过throw将产生的异常抛出。如果一个方法可能会出现异常,但没有能力处理这种异常,可以在方法声明处用throws子句来声明抛出异常。谁调用这个方法则谁处理抛出的异常。
throws 后面可以跟多个异常类型,多个异常使用逗号隔开。当方法抛出异常时,方法将不对这些类型及其子类类型异常做处理,而抛向调用该方法的方法,由他去处理。

  1. public class TryTest {
  2. public static void main(String[] args) {
  3. try {
  4. int result = test();
  5. System.out.println("运行的结果为:" + result);
  6. } catch(ArithmeticException e) {
  7. System.out.println("要求除数不能为0");
  8. e.printStackTrace();
  9. } catch(Exception e) {
  10. e.printStackTrace();
  11. }
  12. //test();写exception异常后会有提示
  13. }
  14. public static int test() throws ArithmeticException, Exception{//告诉编译器该方法可能会发生何种异常,可以使用多异常或写Exception
  15. Scanner input = new Scanner(System.in);
  16. System.out.println("======运算开始=======");
  17. System.out.println("请输入第一个整数");
  18. int one = input.nextInt();
  19. System.out.println("请输入第二个整数");
  20. int two = input.nextInt();
  21. System.out.println("one和two的商是:" + one / two);
  22. System.out.println("运算结束");
  23. return one / two;
  24. }
  25. }
  1. package com.dodoke;
  2. import java.util.InputMismatchException;
  3. import java.util.Scanner;
  4. public class ExceptionClass {
  5. public static void main(String[] args) {
  6. try {
  7. int result = test();
  8. System.out.println("运行的结果为:" + result);
  9. } catch(ArithmeticException e) {
  10. System.out.println(e.getMessage());
  11. }
  12. //test();写exception异常后会有提示
  13. }
  14. public static int test() {//告诉编译器该方法可能会发生何种异常,可以使用多异常或写Exception
  15. Scanner input = new Scanner(System.in);
  16. System.out.println("======运算开始=======");
  17. System.out.println("请输入第一个整数");
  18. int one = input.nextInt();
  19. System.out.println("请输入第二个整数");
  20. int two = input.nextInt();
  21. if (two == 0){
  22. throw new ArithmeticException("除数不能为0");
  23. }
  24. System.out.println("one和two的商是:" + one / two);
  25. System.out.println("运算结束");
  26. return one / two;
  27. }
  28. }

如果方法抛出的是非检查异常,那么调用此方法处编译器不会强制要求进行异常处理(不会报错),如果方法抛出的异常是检查异常或者Exception,则调用此方法处编译器会强制要求进行异常处理(不处理会报错)。针对不报错的情况,可以在抛出异常的方法处加文档注释,这样调用此方法处虽然编译器不会提示要异常处理,但是文档注释会提示。

throw抛出异常对象

注意,throw抛出的是异常对象。throw抛出的只能是Throwable或者是其子类的实例对象。

  1. throws往上抛
  2. public class TryTest {
  3. //酒店入住规则:18以下和80岁以上必须有亲友陪同,不得单独入住
  4. public static void testAge() throws Exception {
  5. System.out.println("请输入年龄:");
  6. Scanner input = new Scanner(System.in);
  7. int age = input.nextInt();
  8. if(age < 18 || age > 80) {
  9. throw new Exception("18岁以下,80岁以上的住客必须由亲友陪同入住");
  10. } else {
  11. System.out.println("欢迎入住本酒店!");
  12. }
  13. }
  14. }
  15. 自己抛出自己处理
  16. public class TryTest {
  17. //酒店入住规则:18以下和80岁以上必须有亲友陪同,不得单独入住
  18. public static void testAge() {
  19. System.out.println("请输入年龄:");
  20. Scanner input = new Scanner(System.in);
  21. int age = input.nextInt();
  22. if(age < 18 || age > 80) {
  23. try {
  24. throw new Exception("18岁以下,80岁以上的住客必须由亲友陪同入住");
  25. } catch (Exception e) {
  26. e.printStackTrace();
  27. }
  28. } else {
  29. System.out.println("欢迎入住本酒店!");
  30. }
  31. }
  32. }

throw抛出异常对象的处理方案:

  1. 自己抛出自己处理,通过try-catch包含throw语句。
  2. 用throws往上抛,调用者可以try-catch处理或者继续往上抛。throws抛出异常类型时,要抛出与throw对象相同的类型或者其父类,但不能是子类

PS:throw手动抛出的异常不建议使用非检查类型,因为编译器不提示

自定义异常

使用Java内置的异常类可以描述在编程时出现的大部分异常情况。也可以通过自定义异常描述特定业务产生的异常类型。所谓自定义异常,就是定义一个类,去继承Throwable类或者它的子类。
比如,在之前酒店的例子中,自己新设置的异常信息被多次使用,就可以创建一个异常类

  1. public class HotelAgeException extends Exception{
  2. public HotelAgeException (){
  3. super("18岁以下住店必须由亲友陪同");
  4. }
  5. }

getMessage()方法可以显示异常信息

异常链

  1. public class TryClient {
  2. public static void main(String[] args) {
  3. try {
  4. testThree();
  5. } catch (Exception e) {
  6. e.printStackTrace();
  7. }
  8. }
  9. public static void testOne() throws HotelAgeException {
  10. throw new HotelAgeException();
  11. }
  12. public static void testTwo() throws Exception {
  13. try {
  14. testOne();
  15. } catch(HotelAgeException e) {
  16. throw new Exception("我是新产生的异常1",e);//java中保留异常的机制
  17. }
  18. }
  19. public static void testThree() throws Exception {
  20. try {
  21. testTwo();
  22. } catch(Exception e) {
  23. //throw new Exception("我是新产生的异常2",e);或写成另一种方式
  24. Exception e1 = new Exception("我是新产生的异常2");
  25. e1.initCause(e);
  26. throw e1;
  27. }
  28. }
  29. }

异常链就是一个异常接着一个异常。最后输出的异常就只有最后一个。
如果想要把前面所有异常都输出的话,则需要通过保留异常的方法:

  1. 通过构造方法对旧异常对象的获取
    Throwable(String message, Throwable cause) — 保留底层异常的异常信息。
  2. 通过initCause(Throwable cause)方法(一个异常的信息来初始化一个新的异常)用来获取原始异常的描述信息,其中cause是原始异常的对象