一、概述

  • 异常机制已经成为判断一门编程语言是否成熟的标准,异常机制可以使程序中异常处理代码和正常业务代码分离,保证程序代码更加优雅,并提高程序健壮性。
  • 在使用计算机语言进行项目开发的过程中,即使程序员将代码写得 尽善尽美 ,在系统的运行过程中依然会遇到一些问题,因为很多问题不是靠代码能够避免的,比如:客户输入数据的格式读取文件是否存在网络是否保持畅通 等。
  • 异常:在 Java 语言中,将程序执行中发生的不正常的情况称为 **异常**
  • Java异常的处理主要依赖于try,catch,finally,throws,throw这五个关键字
  • 对于异常,一般有两种解决方法:
    • ① 遇到错误就终止程序的执行。
    • ② 由程序员在编写程序的时候,就考虑到错误的检测、错误消息的提示、以及错误的处理。
  • Java 将不同的异常给出不同的对象标识,一般发生某种异常,就创建该异常类型的对象,并抛出;然后程序员就可以捕获到这个异常对象,并处理;如果没有捕获到这个异常对象,那么这个异常对象将会导致程序的终止。

    注意:开发过程中的语法错误和逻辑错误不是异常。

二、异常体系

2.1 概述

  • Java对异常的处理是按异常分类处理的,不同异常有不同的分类,每种异常都对应一个类型(class),每个异常都对应一个异常(类的)对象
  • 异常的根类是 java.lang.Throwable ,其下有两个子类:java.lang.Errorjava.lang.Exception ,平常我们所说的异常一般都是指java.lang.Exception
  • java的系统定义的大致的异常类的层次图如下:
  • Throwable

Throwable.png

  • 简略图

image.png

  • Error :严重错误,无法处理的错误,只能事先避免
    • 例如:StackOverflowError 、OOM( OutOfMemoryError )、内存泄漏( Memory Leak )。
    • 内存溢出( OOM ):是指应用程序中存在无法回收的内存或者使用的内存过多,最终是的程序运行用到的内存大于要提供的最大内存。
    • 内存泄露( Memory Leak ):是指程序中已经动态分配的堆内存由于某种原因没有来得及释放或无法释放,造成一系统内存的浪费,导致程序运行速度减慢甚至导致系统崩溃等严重后果。
  • Exception :表示异常,我们平常所说的异常就是指 Exception ,因为这类异常一旦出现,我们就需要对代码进行更正,修复程序。
  • 异常 - 图3
    • 编译时期异常( checked 异常)
      • 例如:FileNotFoundException
      • 是指编译器要求必须处置的异常。即程序在运行时由于外界因素造成的一 般性异常。
      • 编译器要求 Java 程序必须捕获或声明所有编译时异常。
      • 对于这类异常,如果程序出处理,可能会带来意向不到的结果。
    • 运行时期异常(runtime 异常)
      • java.lang.RuntimeException 类及它的子 类都是运行时异常。
      • 是指编译器不要求强制处置的异常。一般是指编程时的逻辑错误,程序员应该积极避免其出现的异常。
      • 对于这类异常,可以不做处理,因为这类异常很普遍,如果全处理可能会对程序的可读性和运行效率产生影响。

        注意:也有人将异常分为 非runtime异常runtime异常

2.2 Throwable 中的常用方法

方法名 说明
public String getMessage() 返回此 throwable 的详细消息字符串
public String toString() 返回此可抛出的简短描述
public void printStackTrace() 将该异常的跟踪栈信息输出到标准错误输出在控制台(异常链)
  1. public class ExceptionDemo02 {
  2. public static void main(String[] args) {
  3. System.out.println("开始");
  4. method();
  5. System.out.println("结束");
  6. }
  7. public static void method() {
  8. try {
  9. int[] arr = {1, 2, 3};
  10. System.out.println(arr[3]); //new ArrayIndexOutOfBoundsException();
  11. System.out.println("这里能够访问到吗");
  12. } catch (ArrayIndexOutOfBoundsException e) {
  13. //new ArrayIndexOutOfBoundsException();
  14. //e.printStackTrace();
  15. //public String getMessage():返回此 throwable 的详细消息字符串
  16. // System.out.println(e.getMessage());
  17. //Index 3 out of bounds for length 3
  18. //public String toString():返回此可抛出的简短描述
  19. //System.out.println(e.toString());
  20. //java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
  21. //public void printStackTrace():把异常的错误信息输出在控制台
  22. e.printStackTrace();
  23. //java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
  24. //at com.itheima_02.ExceptionDemo02.method(ExceptionDemo02.java:18)
  25. //at com.itheima_02.ExceptionDemo02.main(ExceptionDemo02.java:11)
  26. }
  27. }
  28. }

三、异常的抛出机制

3.1 概述

  • Java 提供的是异常处理的 抓抛模型
  • Java 程序的执行过程中如果出现了异常,会产生一个 异常类对象 ,该异常对象将被提交给 JVM ,这个过程称为 抛出异常
  • 异常对象的生成:

    • ① 由 JVM 自动生成 :程序运行结果中,JVM 检测到程序发生了问题,如果在当前代码中没有找到相应的处理程序,就会在后台自动创建一个对应的异常类的实例对象并抛出(自动抛出)。
    • ② 由开发人员 手动抛出:由开发人员通过 throw new Exception对象throw new Exception的子类对象 手动抛出异常。
  • 异常抛出机制:

image.png

3.2 JVM 默认处理异常的方式

如果程序出现了问题,我们没有做任何处理,最终JVM 会做默认的处理,处理方式有如下两个步骤:

  • 调用异常的对象的**printStackTrace()**方法,把异常的名称,错误原因及异常出现的位置等信息输出在了控制台
  • 程序停止执行,哪里出现异常,异常就在哪里处理

    如果出现异常的线程为主线程,则整个程序运行终止;如果非主线程,则终止该线程,其他线程继续运行。

  • 示例:

    /**
    * 发生异常的时候,如果没有进行处理,会一层一层的向上抛出,
    最终交给JVM处理,JVM会终止程序,输出对应的信息。
    */
    public class TestException {
      public static void main(String[] args) {
          System.out.println("main方法");
          show();
      }
    
      public static void show() {
          System.out.println("show方法");
          int[] arr = {1, 2};
          System.out.println(arr[2]);
          System.out.println("show方法调用结束");
      }
    }
    

3.3 查看异常信息

控制台在打印异常信息时,会打印异常类名,异常出现的原因,异常出现的位置
我们调bug时,可以根据提示,找到异常出现的位置,分析原因,修改异常代码
image.png

四、异常的处理

为了保证程序的正常执行,代码必须对可能出现的异常进行处理。

4.1 异常处理的方式

  • ① throw 和 throws
  • ② try..catch..finally

4.2 throws 声明异常

  • 声明异常:将异常标识出来,告诉调用者当前方法不处理异常,你调用我,有可能会出现XXX的异常,你来处理这个异常。调用者可以选择捕获或者抛出,如果所有方法(包括main)都选择抛出。那么最终将会抛给JVM。JVM打印出栈轨迹(异常链)。

  • 定义格式 ```java 修饰符 返回值类型 方法() throws 异常类名1,异常类名2… {

}


- 示例代码
```java
public class ExceptionDemo {
    public static void main(String[] args) throws ParseException{
        System.out.println("开始");
          //method();
          method2();

        System.out.println("结束");
    }

    //编译时异常
    public static void method2() throws ParseException {
        String s = "2048-08-09";
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        Date d = sdf.parse(s);
        System.out.println(d);
    }

    //运行时异常
    public static void method() throws ArrayIndexOutOfBoundsException {
        int[] arr = {1, 2, 3};
        System.out.println(arr[3]);
    }
}
import java.text.ParseException;
import java.text.SimpleDateFormat;

public class ExceptionDemo6 {
    public static void main(String[] args) throws ParseException {
        method1(); //此时调用者也没有处理.还是会交给虚拟机处理.
        method2(); //还是继续交给调用者处理.而main方法的调用者是虚拟机还是会采取虚拟机默认处理异常的方法.
    }

    //告诉调用者,你调用我,有可能会出现这样的异常哦.
    //如果方法中没有出现异常,那么正常执行
    //如果方法中真的出现了异常,其实也是将这个异常交给了调用者处理.
    //如果声明的异常是一个运行时异常,那么声明的代码可以省略
    private static void method1() /*throws NullPointerException*/ {
        int [] arr = null;
        for (int i = 0; i < arr.length; i++) {//出现的空指针异常,还是由虚拟机创建出来的.
            System.out.println(arr[i]);
        }
    }

    //告诉调用者,你调用我,有可能会出现这样的异常哦.
    //如果方法中没有出现异常,那么正常执行
    //如果方法中真的出现了异常,其实也是将这个异常交给了调用者处理.
    //如果声明的异常是一个编译时异常,那么声明的代码必须要手动写出.
    private static void method2() throws ParseException {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日");
        sdf.parse("2048-10月10日");
    }
}

注意事项

  • 这个throws格式是跟在方法的括号后面的
  • 运行时异常因为在运行时才会发生,所以在方法后面可以不写,运行时出现异常默认交给jvm处理
  • 编译时异常必须要进行处理,两种处理方案:throws 或者 try...catch ... ; 如果采用 throws 这种方案,在方法上进行显示声明,将来谁调用这个方法谁处理

4.3 throw 抛出异常

  • 用于抛出一个具体的异常对象。

    thorw new 异常类名(实参);
    
  • 抛出异常的意义

    • 在方法中,当传递的参数有误,没有继续运行下去的意义了,则采取抛出异常,表示让该方法结束
    • 告诉调用者方法中出现了问题

示例代码

public class ExceptionDemo8 {
    public static void main(String[] args) {
        //int [] arr = {1,2,3,4,5};
        int [] arr = null;
        printArr(arr);//就会接收到一个异常.
                      //我们还需要自己处理一下异常.
    }

    private static void printArr(int[] arr) {
        if(arr == null){
            //调用者知道成功打印了吗?
            //System.out.println("参数不能为null");
            throw new NullPointerException(); //当参数为null的时候,手动创建了一个异常对象,抛给了调用者,产生了一个异常
        }else{
            for (int i = 0; i < arr.length; i++) {
                System.out.println(arr[i]);
            }
        }
    }
}

4.4 throws和throw的区别

throws throw
用在方法声明后面,跟的是异常类名 用在方法体内,跟的是异常对象
表示声明异常,调用该方法有可能会出现这样的异常 表示手动抛出异常对象,由方法体内的语句处理

注意

  • throw语句后不允许有紧跟其他语句,因为这些没有机会执行
  • 如果一个方法调用了另外一个声明抛出异常的方法,那么这个方法要么处理异常,要么声明抛出
  • 当throw语句抛出的异常是Checked异常,则该throw语句要么处于try块里显式捕获该异常,要么放在一个带throws声明抛出的方法中,即把异常交给方法的调用者处理。
  • 当throw语句抛出的异常是Runtime异常,则该语句无须放在try块内,也无须放在带throws声明抛出的方法中,程序既可以显式使用try…catch来捕获并处理该异常,也可以完全不理会该异常,把该异常交给方法的调用者处理。

示例代码

public class TestException {  
    public static void throw_checked(int a) throws Exception{
        //Exception默认为checkedExcption
        if(a>0) throw new Exception("Exception:a>0");
    }
    public static void throw_runtime(int a) {
        if(a>0) throw new RuntimeException("runtimeException:a>0");
    }
    public static void main(String[] args) {  
        int a=1;
        try {
            throw_checked(a);
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        throw_runtime(a);
    }  
}

可见Runtime异常的灵活性比Checked的灵活性更强。因为Checked异常必须要被显式捕获或者显式抛出,所以Runtime写的更方便,我们自定义异常一般都是用Runtime异常。

4.5 try-catch 捕获异常

java提出了一种假设,如果try中的语句一切正常那么将不执行catch语句块,如果try中语句出现异常,则会抛出异常对象,由catch语句块根据自己的类型进行捕获。若没有相应的catch块,则抛出。

try{
......  //可能产生异常的代码 }
catch( ExceptionName1 e ){
......  //当产生ExceptionName1型异常时的处置措施 }
catch( ExceptionName2 e ){
......    //当产生ExceptionName2型异常时的处置措施 }
finally{
......     //无论是否发生异常,都无条件执行的语句 
}

解释:

  • try :捕获异常的第一步是用 try{} 语句块选定捕获异常的范围,将可能出现异常的代码放到 try 语句块中。
  • catch(Exception e)
    • catch 语句块中是对 异常对象 进行处理的代码。每个 try 语句块可以伴随 一个或多个catch 语句,用于处理可能产生的 不同类型 的异常对象。
    • 如果明确知道产生的是何种异常,可以用该异常类作为 catch 的参数,也可以用其父类作为 catch 的参数。
    • 多个 catch 语句块,子类型在上,父类型在下。
  • finally
    • 捕获异常的最后一步是通过 finally 语句为异常处理提供一个统一的出口,使得在程序流程到其它部分以后,能够对程序的状态做出统一的处理。
    • 不论在try代码块中是否发生了异常,catch语句中是否执行,catch语句中是否有异常,catch语句中是否有return,finally块中的语句都会被执行。

示例代码

import java.io.File;
import java.io.FileNotFoundException;

/**
 * @author lhl
 */
public class TestException {
    public static void main(String[] args) {
        readFile("不敲代码学会Java秘籍.txt");
        System.out.println("继续学习吧...");
    }

    public static void readFile(String filePath) {
        File file = new File(filePath);
        try {
            if (!file.exists()) {
                throw new FileNotFoundException(filePath + "文件不存在");
            }
            if (!file.isFile()) {
                throw new IllegalAccessException(filePath + "非法访问");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println("这里的代码一定会执行");
        }

    }
}

注意

  1. 如果 try 中没有遇到问题,怎么执行?
    会把try中所有的代码全部执行完毕,不会执行catch里面的代码
  2. 如果 try 中遇到了问题,那么 try 下面的代码还会执行吗?
    那么直接跳转到对应的catch语句中,try下面的代码就不会再执行了
    当catch里面的语句全部执行完毕,表示整个体系全部执行完全,继续执行下面的代码
  3. 如果出现的问题没有被捕获,那么程序如何运行?
    那么try…catch就相当于没有写.那么也就是自己没有处理.
    默认交给虚拟机处理.
  4. 同时有可能出现多个异常怎么处理?
    出现多个异常,那么就写多个catch就可以了.
  5. 如果多个异常之间存在子父类关系.那么父类一定要写在下面
  6. 当在 try 或者 catch 中调用退出 JVM 的相关方法,例如 System.exit(0) ,此时 finally 不会执行,否则 finally 永远会执行。

五、自定义异常

5.1 概述

  • Java 中的不同异常类,分别表示某一种具体的异常情况,但是在开发中总会有些异常情况是 Java 中预先没有定义好的,此时我们需要根据自己业务的异常情况来定义异常类,比如:年龄负数问题,考试成绩负数问题等等。

  • 自定义目的就是见名知意

  • 学习观看其他异常类如何写的

    • ArithmeticException
    • ArrayIndexOutOfBoundsException
    • IndexOutOfBoundsException
  • 实现步骤

1、定义异常类
2、写继承关系
自定义编译时异常类:自定义类并继承于 java.lang.Exception
自定义运行时异常类:自定义类并继承于 java.lang.RuntimeException
(实际开发中,这种用的很多)。
3、提供空参构造
4、提供带参构

  • 代码实现

    public class AgeOutOfBoundsException extends RuntimeException {
      public AgeOutOfBoundsException() {
      }
    
      public AgeOutOfBoundsException(String message) {
          super(message);
      }
    }
    

    5.2 异常的练习

  • 需求
    键盘录入学生的姓名和年龄,其中年龄为18 - 25岁,超出这个范围是异常数据不能赋值.需要重新录入,一直录到正确为止

  • 实现步骤
    1. 创建学生对象
    2. 键盘录入姓名和年龄,并赋值给学生对象
    3. 如果是非法数据就再次录入
  • 代码实现

    学生类

    public class Student {
     private String name;
     private int age;
    
     public Student() {
     }
    
     public Student(String name, int age) {
         this.name = name;
         this.age = age;
     }
    
     public String getName() {
         return name;
     }
    
     public void setName(String name) {
         this.name = name;
     }
    
     public int getAge() {
         return age;
     }
    
     public void setAge(int age) {
         if(age >= 18 && age <= 25){
             this.age = age;
         }else{
             //当年龄不合法时,产生一个异常
             throw new RuntimeException("年龄超出了范围");
         }
     }
    
     @Override
     public String toString() {
         return "Student{" +
                 "name='" + name + '\'' +
                 ", age=" + age +
                 '}';
     }
    }
    
    public class AgeOutOfBoundsException extends RuntimeException {
     public AgeOutOfBoundsException() {
     }
    
     public AgeOutOfBoundsException(String message) {
         super(message);
     }
    }
    
    public class ExceptionDemo12 {
     public static void main(String[] args) {
         // 键盘录入学生的姓名和年龄,其中年龄为 18 - 25岁,
         // 超出这个范围是异常数据不能赋值.需要重新录入,一直录到正确为止。
    
         Student s = new Student();
    
         Scanner sc = new Scanner(System.in);
         System.out.println("请输入姓名");
         String name = sc.nextLine();
         s.setName(name);
        while(true){
            System.out.println("请输入年龄");
            String ageStr = sc.nextLine();
            try {
                int age = Integer.parseInt(ageStr);
                s.setAge(age);
                break;
            } catch (NumberFormatException e) {
                System.out.println("请输入一个整数");
                continue;
            } catch (AgeOutOfBoundsException e) {
                System.out.println(e.toString());
                System.out.println("请输入一个符合范围的年龄");
                continue;
            }
            /*if(age >= 18 && age <=25){
                s.setAge(age);
                break;
            }else{
                System.out.println("请输入符合要求的年龄");
                continue;
            }*/
        }
         System.out.println(s);
    
     }
    }