Java语言基础(SE)-第五节 异常(Exception)

异常简介

  1. 开发中的错误: 在开发 Java 程序的过程中,会遇到各种各样的错误
    1. 语法错误:会导致编译失败,程序无法正常运行
    2. 逻辑错误:比如需要执行加法操作时,不小心写成了减法操作
    3. 运行时错误:在程序运行过程中产生的意外,会导致程序终止运行
  2. Java 异常强制用户考虑程序的强健性和安全性。异常处理不应用来控制程序的正常流程,其主要作用是捕获程序在运行时发生的异常并进行相应处理。

异常的种类

  1. 所有的异常最终都继承自 java.lang.Throwable

                                             Object
                                             Throwable 
                             Exception                       Error
     RuntimeException(运行时异常) (非运行时异常)...           ...
    
    1. Throwable 类是所有异常和错误的超类,下面有 Error 和 Exception 两个子类分别表示错误和异常。其中异常类 Exception 又分为运行时异常和非运行时异常,这两种异常有很大的区别,也称为非检查异常(Unchecked Exception)和检查异常(Checked Exception)。
    2. Exception 类用于用户程序可能出现的异常情况,它也是用来创建自定义异常类型类的类。
    3. Error 定义了在通常环境下不希望被程序捕获的异常。Error 类型的异常用于 Java 运行时由系统显示与运行时系统本身有关的错误。通常是灾难性的致命错误,不是程序可以控制的。堆栈溢出是这种错误的一例。
  2. 检查型异常(Checked Exception)
    1. 这类异常一般难以避免,编译器会进行检查:如果开发者没有处理这类异常,编译器将会报错
    2. 指 Error、RuntimeException 以外的异常,类型上都属于 Exception 类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如 IOException、ClassNotFoundException 等以及用户自定义的 Exception 异常,一般情况下不自定义检查异常。表 1 列出了一些常见的异常类型及它们的作用。
  3. 非检查型异常(Unchecked Exception)
    1. 这类异常一般可以避免,编译器不会进行检查:如果开发者没有处理这类异常,编译器将不会报错
    2. 都是 RuntimeException 类及其子类异常,如 NullPointerException、IndexOutOfBoundsException 等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般由程序逻辑错误引起,程序应该从逻辑角度尽可能避免这类异常的发生。
    3. 哪些异常是非检查型异常?:Error、RuntimeException以及其子类
  4. 举例:

     ******检查型异常**********
     //java.io.FileNotFoundException 文件不存在
     FileOutputStream fos = new FileOutputStream("F://xxx.txt");
     //反射
     //java.lang.ClassNotFoundException,不存在这个类
     Class cls = Class.forName("Dog");
     //java.lang.InstantiationException,没有无参的构造函数
     //java.lang.IllegalAccessException,没有权限访问构造函数
     Dog dog = (Dog) cls.newInstance();
        
     ******非检查型异常**********
     //Error
     //java.lang.OutOfMemoryError,内存不够用
     for (int i = 0; i < 200; i++) {
         long[] a = new long[1000000000];
     }
        
     //RuntimeException
      //java.lang.NullPointerException,使用了空指针
     StringBuilder s = null;
     s.append("abc");
        
     //java.lang.NumberFormatException数字的格式不对
     Integer i = new Integer("abc");
        
     int[] array = {11,22,33};
     //java.lang.ArrayIndexOutOfBoundsException,数组索引越界
     array[4] = 44;
        
     Object obj = "123.4";
     //java.lang.ClassCastException:类型不匹配
     Double d = (Double)obj;
    
    1. 总结: 检查就是编译器对否会自动检查,这个方法是否会抛出异常、以及抛出异常的种类是否需要开发者必须用try-cach或者throws来处理。检查型异常类型必须处理,非检查型类异常可以不处理
  5. 常见的异常类型及它们的作用。

     Exception	                       异常层次结构的根类
     RuntimeException	               运行时异常,多数 java.lang 异常的根类
     ArithmeticException	            算术谱误异常,如以零做除数
     ArraylndexOutOfBoundException    数组大小小于或大于实际的数组大小
     NullPointerException	            尝试访问 null 对象成员,空指针异常
     ClassNotFoundException	         不能加载所需的类
     NumberFormatException	          数字转化格式异常,比如字符串到 float 型数字的转换无效
     IOException	                    I/O 异常的根类
     FileNotF oundException	         找不到文件
     EOFException	                    文件结束
     InterruptedException	            线程中断
     IllegalArgumentException	       方法接收到非法参数
     ClassCastException	            类型转换异常
     SQLException	                    操作数据库异常
    

异常处理

  1. 程序产生了异常,一般称之为:抛出了异常
    1. 如果没有主动去处理它,会导致程序终止运行
  2. 异常产生后默认的处理过程解析(自动处理的过程):
    1. 默认会在出现异常的代码那里自动创建一个异常对象:ArithmeticException
    2. 异常会从方法中出现的点这里抛出给调用者,调用者最终抛出给JVM虚拟机
    3. 虚拟机接收到异常对象后,先在控制台直接输出异常栈信息数据
    4. 直接从当前执行的异常点结束当前程序
    5. 后续代码没有机会执行,因为程序已经死亡
  3. 异常处理有2种方法
    1. try-catch: 捕获异常
    2. throws: 将异常往上抛

try-catch

  1. 示例代码

     try {
         代码1
         代码2 (可能会抛出异常) 
         代码3
     } catch (异常A e) {
         //当抛出【异常A】类型的异常时,会进入这个代码块
     }catch (异常B e) {
         //当没有抛出【异常A】类型 
         //但抛出【异常B】类型的异常时,会进入这个代码块
     }catch (异常C e) {
         //当没有抛出【异常A】、【异常B】类型 
         //但抛出【异常C】类型的异常时,会进入这个代码块
     }
     代码4
    
    1. 如果【代码2】没有抛出异常
      1. 【代码1、3】都会被执行
      2. 所有的 catch 都不会被执行
      3. 【代码4】会被执行
    2. 如果【代码2】抛出异常
      1. 【代码1】会被执行、【代码3】不会被执行
      2. 会选择匹配的 catch 来执行代码
      3. 【代码4】会被执行
    3. 父类型的异常必须写在子类型的后面
      1. 【异常A】不可以是【异常B、C】的父类型
      2. 【异常B】不可以是【异常C】的父类型
  2. 异常对象的常用方法

     try {
         Integer i = new Integer("abc");
     } catch (NumberFormatException e) {
         //异常描述
         System.out.println(e.getMessage());
         //异常名称 异常描述
         System.out.println(e);
         //打印堆栈信息
         e.printStackTrace();
     }
    
  3. 一个 catch 捕获多种类型的异常

    1. 从 Java 7 开始,单个 catch 可以捕获多种类型的异常
    2. 如果并列的几个异常类型之间存在父子关系,保留父类型即可
    3. 这里的变量 e 是隐式 final 的,就是e不能再次赋值了
     try {
     } catch (异常A | 异常B | 异常C  e) {
         //当抛出【异常A】或【异常B】或【异常C】类型的异常时,会进入这个代码块
     }
    

finally

  1. try 或 catch 正常执行完毕后,一定会执行 finally 中的代码
    1. finally 可以和 try-catch 搭配使用,也可以只和 try 搭配使用
    2. 经常会在 finally 中编写一些关闭、释放资源的代码(比如关闭文件)
     try {
     } catch (异常e) {
     }finally {
     }
     try {
     }finally {
     }
    
  2. 举例:

     PrintWriter  out = null;
     try {
         out = new PrintWriter("F:/zh.txt");
         //向文件中写入内容
         out.print("My name is ZH");
     } catch (FileNotFoundException e) {
         e.printStackTrace();
     } finally {
         if (out != null) {
             //关闭文件
             out.close();
         }
     }
    
  3. finally 细节
    1. 如果在执行 try 或 catch 时,JVM 退出或者当前线程被中断、杀死
      1. finally 可能不会执行
    2. 如果 try 或 catch 中使用了 return、break、 continue 等提前结束语句
      1. finally 会在 return、break、continue 之前执行
    3. 下面代码的打印结果是什么?

       for (int i = 1; i <= 3; i++) {
           try {
               System.out.println(i + "_try_1");
               if (i== 2) continue;
               System.out.println(i + "_try_2");
           } finally {
               System.out.println(i + "_finally");
           } 
       }
      
      1. 上面是continue,则打印结果

         1_try_1
         1_try_2
         1_finally
         2_try_1
         2_finally
         3_try_1
         3_try_2
         3_finally
        
      2. 如果是break

         1_try_1
         1_try_2 
         1_finally 
         2_try_1 
         2_finally
        
    4. 代码举例:

       static int get() {
           try {
               new Integer("abc");
               System.out.println(1);
               return 2;
           } catch (Exception e) {
               System.out.println(3);
               //return之前要先去调用finally!!!
               return 4;
           } finally {
               System.out.println(5);
           }
       }
              
       public static void main(String[] args) { 
           System.out.println(get());
       }
       //打印结果: 3 5 4
      

throws

  1. throws 的作用:将异常抛给上层方法
  2. 如果 throws 后面的异常类型存在父子关系,保留父类型即可 图1
    1. 如果异常最终抛给了 JVM,那么整个 Java 程序将终止运行

       //案例x
       public static void main(String[] args)  throws ClassNotFoundException { 
           System.out.println(1);
           //没有try-catch,往JVM抛,JVM终止程序
           method1();
           //不会执行
           System.out.println(2);
       }
              	
       static void method1() throws ClassNotFoundException {
           //没有try-catch,往上层抛
           method2();
       }
       static void method2() throws ClassNotFoundException {
           //没有try-catch,往上层抛
           method3();
       }
       static void method3() throws ClassNotFoundException  {
           //没有try-catch,往上层抛
           //java.lang.ClassNotFoundException,不存在这个类,编译时异常
           Class cls = Class.forName("Dog");
       }
      
  3. 可以一部分异常使用 try-catch 处理,另一部分异常使用 throws 处理

     static void methodx() throws FileNotFoundException {
         PrintWriter out = new PrintWriter("F:/zh.txt");
         try {
             Class cls = Class.forName("Dog");
         } catch (Exception e) {
             e.printStackTrace();
         }
     }
    
  4. 总结: throws是不想处理的异常,则抛到上一层处理。try-catch处理当前异常
  5. throws 的细节
    1. 当父类的成员方法没有 throws 异常
      1. 子类的重写方法也不能 throws 异常
    2. 当父类的成员方法有 throws 异常
      1. 子类的重写方法可以
        1. 不 throws 异常
        2. throws 跟父类一样的异常
        3. throws 父类异常的子类型
     public class Person {
         public void test1() {};
         public void test2() throws IOException {};
         public void test3() throws IOException {};
         public void test4() throws IOException {};
     }
     public class Student extends Person {
         @Override
         public void test1() {};
         @Override
         public void test2() {};
         @Override
         public void test3() throws IOException {};
         @Override
         public void test4() throws FileNotFoundException {};
     }
    

throw

  1. 使用 throw 可以抛出一个新建的异常

     public class Person {
         public Person(String name) throws Exception {
             if (name == null || name.length() == 0) {
                 throw new Exception("name must not be empty");
             }
         }
     }
     public static void main(String[] args) {
         try {
             //这里就必须处理异常了
             Person person = new Person("zh");
         } catch (Exception e) {
             e.printStackTrace();
         }
     }
    

总结:非检查型(运行时)异常、检查型(编译时)异常处理机制

  1. 检查型(编译时)异常处理方式:必须处理
    1. 方式一:在编译出现异常的地方,直接通过try-catch进行捕获异常然后处理。
    2. 方式二:在最外层方法实现try-catch捕获处理异常,在每层子方法后手工加上throws 异常类型 来向外抛出异常(比如案例x
  2. 非检查型(运行时)异常:可以不用处理,默认会层层抛出自动往外层方法抛异常(默认throws RuntimeException)不需要手工抛出(案例x中所有的throws都不需要添加),处理规范直接在最外层方法通过try-catch捕获处理异常。也可以直接在异常处使用try-catch。
  3. 区别:
    1. 编译异常和运行时异常都可以用try-catch、throws来处理,但是使用throws时,编译时异常需要手工添加,运行时不需要手工添加。
    2. 即检查型异常编译器强制要求通过try-catch或throws去处理,非检查型异常编译器不强制要求
    3. 异常只要最终传递到了JVM中,都会导致程序终止运行 图1

自定义异常:

  1. 开发中自定义的异常类型,基本都是以下 2 种做法
    1. 继承自 Exception—检查型异常,必须处理
      1. 使用起来代码会稍微复杂
      2. 希望开发者重视这个异常、认真处理这个异常
      3. 开发者要么try-catch,要么throws
    2. 继承自 RuntimeException—非检查型异常,可以不处理
      1. 使用起来代码会更加简洁
      2. 不严格要求开发者去处理这个异常
      3. 开发者可以不用处理异常
  2. 代码举例:

     public class EmptyNameException extends RuntimeException {
         public EmptyNameException() {
             super("name must not be empty");
         }
     }
     public class WrongAgeException extends RuntimeException {
         private int age;
         public WrongAgeException(int age) {
             super("wrong age:" + age + ", age must be >0");
         }
     }
     public class Person {
         private String name;
         private int age;
         public Person(String name,int age) {
             if(name == null || name.length() ==0) {
                 throw new EmptyNameException();
             }
             if(age<0) {
                 throw new WrongAgeException(age);
             }
             this.name = name;
             this.age = age;
         }
     }
     //非检查型异常,可以不做处理,但是会透传到
     // com.zh.WrongAgeException: wrong age:-10, age must be >0
     Person person = new Person("ZH", -10);
     //不会执行
     System.out.println(1);
    

使用异常的好处

  1. 将错误处理代码与普通代码区分开
  2. 能将错误信息传播到调用堆栈中
  3. 能对错误类型进行区分和分组

断言

//编写一个断言类
public class Asserts {
    public static void test(boolean v) {
        if(v) return;
        //打印异常,并输出异常的栈位置
        System.err.println(new RuntimeException().getStackTrace()[1]);
        //抛出异常
        //throw new IllegalArgumentException("条件不满足");
    }
}

//使用
int age = 10;
//用于判断结果是否正确,如果正确没有反应,反之,抛出异常
Asserts.test(age>0);
String name = "";
Asserts.test(name != null && name.length() == 0);
Table of Contents