《Java:浅析异常》—— 适宜预习与复习

🎇🎇🎇作者:
@小鱼不会骑车
🎆🎆🎆专栏:
《java练级之旅》
🎓🎓🎓个人简介:
一名专科大一在读的小比特,努力学习编程是我唯一的出路😎😎😎

认识异常

  • 🍂简单认识异常
  • 🍂异常的体系结构
  • 🍂异常的分类
  • 🍃编译时异常
  • 🍃运行时异常
  • 🍃Error
  • 🍂如何处理异常?
  • 🍃异常的抛出
  • 🍃异常的捕获
  • 🍄throws 关键字
  • 🍄try 和 catch 关键字
  • 🍄finally 关键字
  • 🍃异常的处理流程
  • 🍂自定义异常类
  • 🍂简单认识异常

    在java中,将程序执行过程中发生的不正常的行为称之为异常
    例如算数异常,数组下标越界异常,空指针异常,还有可能会涉及到向下转型异常等等,这些代码在编译期间并没有报错,代表着该代码可以顺利编译并且生成.class文件。
    算术异常:

     int c=10/0;
     //执行结果
     // Exception in thread "main" java.lang.ArithmeticException: / by zero
    

    数组越界异常:

     int []array={1,2,3,4,5};
     System.out.println(array[10]);
     //执行结果
     //Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 10
    

    空指针异常:

     int []array=null;
     System.out.println(array.length);
    //执行结果
    // Exception in thread "main" java.lang.NullPointerException
    

    向下转型异常:

    public class Test {
    
        public static void main(String[] args){
            A a=new B();
            C c=(C)a;
        }
        
    }
    //执行结果
    //Exception in thread "main" java.lang.ClassCastException: B cannot be cast to C
    //中文:B不能转换为C
    class A {}
    class B extends A {}
    class C extends A { }
      
    

    对于爆出的错误,可以这么理解

    也可以点击错误信息直接跳转到java的源码,将它的注释复制,去翻译一下,或者去一些技术论坛或者网站去问问别人
    这是一个技术网站:stackoverflow


    最后我们就可以大概知道产生这个错误的原因或者都有什么原因会产生这个错误。

    从上述过程中可以看到,java中不同类型的异常,都有与其对应的类来进行描述。例如算数异常,描述他的类就是在java.long这个包的ArrayIndexOutOfBoundsException中!!

    🍂异常的体系结构

    异常种类繁多,为了对不同异常或者错误进行很好的分类管理,Java内部维护了一个异常的体系结构:


    从上图中可以看到:

    Throwable:是异常体系的顶层类,其派生出两个重要的子类, Error 和 Exception
    Error :指的是Java虚拟机无法解决的严重问题,比如:JVM的内部错误、资源耗尽等,典型代表:StackOverflowError(堆栈溢出错误)和OutOfMemoryError(内存不足错误) ,一旦发生回力乏术。
    Exception :异常产生后程序员可以通过代码进行处理,使程序继续执行。比如:感冒、发烧。我们平时所说 的异常就是Exception

    🍂异常的分类

    异常可能在编译时发生,也可能在程序运行时发生,根据不同时期的不同表现,可以将异常分为:编译时异常 和 运行时异常

    🍃编译时异常

    在程序编译期间发生的异常,称为编译时异常,也称为受检查异常(Checked Exception),此时如果编译器想要通过编译,一定要处理掉这个异常!
    比如我们重写一个Cat类,在这个类中重写Object 类中的 clone 方法。
    由于我们要重写clone这个方法所以我们需要实现克隆这个接口,Cat类里面涉及到的throws我们先不用去在意,这个后面会讲到

    class Cat implements Cloneable {
        @Override
        protected Object clone() throws CloneNotSupportedException {
            return super.clone();
        }
    }
    

    那现在我们已经实现了一个Cat类,是不是我们通过这个Cat类对象的引用就可以去调用Cat类这个对象的clone方法?


    我们先不管这个错误是什么,我们可以发现,在之前的算数异常,数组越界等异常中,我们都是可以顺利编译的,只不过是在运行时出错,但是上图代码,程序还没有运行就已经画了红线,这时候我们再来看错误信息,大概意思是,你这个方法存在异常,你必须在这个方法中处理你的异常,不然是无法通过编译的,此时我们可以称这个异常为编译时异常

    🍃运行时异常

    在程序执行期间发生的异常,称为运行时异常,也称为非受检查异常(Unchecked Exception),就像前面的空指针异常,数组越界异常在运行时编译是可以正常通过的!
    例如下图:

    虽然最后运行出错,但是还是生成了对应的class文件

    注意:RunTimeException以及其子类对应的异常,都称为运行时异常。比如:NullPointerException、ArrayIndexOutOfBoundsException、ArithmeticException。

    注意:编译时出现的语法性错误,不能称之为异常。 例如下图,我没有加上分号,此时编译过程中就会出错, 这是 “编译期” 出错。而运行时异常指的是程序已经编译通过得到class 文件了, 再由 JVM 执行过程中出现的错误.

    🍃Error

    我们看下述代码,这是一个递归,一个没有结束条件的递归,

        public static void func() {
            func();
        }
        public static void main(String[] args){
           func();
        }
    

    运行之后

    当你的程序爆出这样的错误,其实呢就是你逻辑上的问题了,必须由程序员出手来找到你代码中的问题并解决。

    🍂如何处理异常?

    在Java中,处理异常主要有这五个关键字:

    关键字 作用
    thorw 抛出异常
    try 里面放可能出现的异常代码
    catch 捕捉异常
    finally 必须执行的特定代码
    thorws 声明异常

    🍃异常的抛出

    处理异常的前提是你得有异常,怎么才能有异常?你得抛出异常或者触发异常,下面讲到的就是我们如何抛出异常。
    平时我们的代码运行错误时都是编译器自己抛出异常,那么我可不可以改为自己抛出这个异常?
    当然可以,我们借助 thorw关键字,抛出一个指定的异常对象,将错误信息告诉给调用者。

    语法格式throw new xxxException(“出现异常的原因”);

    当我们想要写一个根据输入的数返回数组下标对应元素的方法时,我们可以根据需求,自己去抛出异常。

    public class Test {
           public static int func(int[] array,int n) {
               if (array == null) {
                   throw new NullPointerException("该数组为空");
               }else if (n >= array.length || n < 0) {
                   throw new ArrayIndexOutOfBoundsException("数组越界");
               }
               System.out.println("运行成功");
               return array[n];
           }
        public static void main(String[] args) {
            int []array={2,4,6,8,10};
            System.out.println(func(array, 6));
        }
    }
    

    当我们的数组为null时就抛出该数组为空的异常。并且并没有执行它后面的代码。

    我们对比一下手动抛出异常和编译器自动抛出异常的区别,用数组越界举例

    并没有什么区别,所以我们可以认为,手动抛出异常就是把编译器默认的异常给显示了出来,也可以称之为显示异常,一般来说,在自己自定义的异常中,会涉及到更多的手动抛出异常,这个后面就会讲到!
    并且当我们想要处理这个异常时,我们只需要解决最顶上的就可以

    但是当我们抛出的是一个受查异常时!编译器就会报错,具体如何解决,在异常声明中会讲到。

    【注意事项】
    ———————————————————————————

  • throw必须写在方法体内部
  • 抛出的对象必须是Exception 或者 Exception 的子类对象
  • 如果抛出的是 RunTimeException(运行时异常) 或者RunTimeException 的子类,则可以不用处理,直接交给JVM来处理
  • 如果抛出的是编译时异常,用户必须处理,否则无法通过编译
  • 异常一旦抛出,其后的代码就不会执行
  • ———————————————————————————

    🍃异常的捕获

    当我们抛出异常时,我们就不会执行后续的代码了,但是我如果不想让程序停下来该怎么办,我想让程序继续执行后续的代码怎么办?就比如我们平时打的王者荣耀,如果一有bug就终止程序,那么我们这款游戏够呛能顺利的运行!接下来讲到的就是如何在遇见异常后,也能让程序继续运行

    🍄throws 关键字

    接下来就是异常的声明了,前面讲到了,当我抛出的是一个受查异常时,我的编译就会报错,那我该如何处理呢?
    我们可以用throws来声明这个异常。
    语法格式

    当我声明了这个异常之后,编译器就没有报错了。
    大概意思就是:我声明了这个异常,我完成我的任务了,如果后面你调用我的这个方法还会抛出异常,那跟我一点关系都没有。并且由于我们声明异常之后,当其他的程序员看到这个代码时,也可以直观的看出这个代码会抛出什么样的异常,这就体现了代码很好的可读性

    接着我们就会发现,我们的main函数中func方法又产生了编译错误。
    原因当方法中抛出编译时异常,用户不想处理该异常,此时就可以借助throws将异常抛给方法的调用者来处理。即当前方法不处理异常,提醒方法的调用者处理异常,如果方法的调用者也不想处理,那么最后就会交给JVM进行处理,但是如果是交给JVM的话,那就是直接终止程序了。怎么处理异常在try-catch讲到。
    当然我们也可以同时声明多个异常

    如果我不想同时声明这么多异常,有一个"一劳永逸"的方法,那就是声明Exception

    public static int func(int[] array,int n) throws Exception{}
    

    由于Exception是所有异常的父类,所以只需要声明Exception就可以,但是这样并不好,因为我很难知道我的方法中究竟是会出现什么异常,所以我们尽量不要为了偷懒写这种垃圾代码。
    并且:即使我的方法都声明了这个异常,最后运行的时候也会因为这个异常直接结束程序,依旧不会执行到后续的代码。

    总结:

    ———————————————————————————

    1. throws必须跟在方法的参数列表之后
    2. 声明的异常必须是 Exception 或者 Exception 的子类
    3. 方法内部如果抛出了多个异常,throws之后必须跟多个异常类型,之间用逗号隔开,如果抛出多个异常类型具有父子关系,直接声明父类即可。
    4. 调用声明抛出异常的方法时,调用者必须对该异常进行处理,或者继续使用throws抛出
      ———————————————————————————

    🍄try 和 catch 关键字

    上面讲到对异常的声明,从根本上来说并没有解决这个问题,只不过是在编译的时候不报错而已,但是逻辑上来说,当我运行的时候,依旧会终止程序。可是有些时候我并不想让这个程序终止,该怎么办呢?这时候就涉及到了异常的捕捉。

    语法格式
    try{
    //
    //将可能出现异常的代码放在这里
    //
    } catch(要捕获的异常类型 e){
    //
    ///如果try中的代码抛出异常了,此处catch捕获时异常类型与try中抛出 的异常类型一致时,或者是try中抛出异常的基类时,就会被捕获到
    // 对异常就可以正常处理,处理完成后,跳出try-catch结构,继续执行后序代码
    }

    // 后序代码
    // 当异常被捕获到时,异常就被处理了,这里的后序代码一定会执行
    // 如果捕获了,由于捕获时类型不对,那就没有捕获到,这里的代码就不会被执行

    如果我catch捕获到的异常和我抛出的异常是一致的,又或者抛出的异常是我要捕捉异常的子类,那么该异常就会被捕捉到,并且对该异常进行处理,处理完成之后跳出这个try-catch语句,继续执行后续的代码!

    关于异常的处理方式

    ———————————————————————————

  • 异常的种类有很多, 我们要根据不同的业务场景来决定.
  • 对于比较严重的问题(例如和算钱相关的场景), 应该让程序直接崩溃, 防止造成更严重的后果
  • 对于不太严重的问题(大多数场景), 可以记录错误日志, 并通过监控报警程序及时通知程序猿
  • 对于可能会恢复的问题(和网络相关的场景), 可以尝试进行重试.
  • 在我们当前的代码中采取的是经过简化的第二种方式. 我们记录的错误日志是出现异常的方法调用信息, 能很
  • 快速的让我们找到出现异常的位置. 以后在实际工作中我们会采取更完备的方式来记录异常信息.
  • ———————————————————————————

    我们依旧用数组举例

    public class Test {
           public static int func(int[] array,int n)  {
               if (array == null) {
                   throw new NullPointerException("该数组为kong");
               }else if (n >= array.length || n < 0) {
                   throw new ArrayIndexOutOfBoundsException("数组越界");
               }
               System.out.println("运行成功");
               return array[n];
           }
        public static void main(String[] args)  {
            int []array={2,4,6,8,10};
            try {
                System.out.println(func(array, 6));
            }catch (NullPointerException e) {
                System.out.println("捕获到了空指针这个异常!!!");
            }catch (ArrayIndexOutOfBoundsException e) {
                System.out.println("抓到了数组越界这个异常");
            }
            System.out.println("我是后续代码.....");
        }
    }
    

    最后运行的结果为

    这里就有三个小问题

    1.为什么我的程序顺利执行下来了?
    2.为什么我的“运行成功”没有打印?
    3.为什么打印了“我是后续代码”?

    问题一
    由于我们抛出的异常和catch中捕获到的异常是一致的,所以该异常被捕获,并不会阻止程序的运行。
    问题二
    上面讲到了不会终止程序的运行,但是为什么没有打印我的“运行成功”这句话?那是因为当我们抛出了这个异常后直接就去判断该异常有没有被捕捉到,如果没有捕捉到就直接结束程序,如果捕捉到了,就跳出try-catch语句,下面用调试来看

    问题三
    由于跳出try-catch语句之后继续执行后续代码,自然也就打印了这句话。

    打印错误信息:如果还想要打印错误信息可以用 e.printStackTrace()来进行打印,我们的e只是一个类型的引用,用这个引用去调用 printStackTrace()方法。

    那么,当我们的数组既是空数组并且又越界的话,会同时抛出两个错误嘛?
    答案是不会的!前面讲到了,当我们的程序抛出第一个异常的时候就会用catch进行捕捉,如果没有捕捉到就会中断程序,只有捕捉到时才会跳出try-catch语句,后续的代码才会执行,从上述的调试中看出来!


    但是对于受查异常来说,如过catch捕捉的是受查异常时,它会判断你的try当中会不会抛出catch中的这个异常,如果没有抛出这个异常,那么你的catch语句就会报错,如下图


    但是如果我们catch中要捕捉的是一个算数异常时,我们的try中并没有抛出这个异常,那么我们的catch会报错嘛?

    答案是不会!因为我们的算数异常属于运行时异常,也可以称为非受查异常,编译器对于运行时异常的监测并没有那么严格,所以我们可以不用在try中抛出这个异常!所以我们可以理解为,如果我们会抛出编译时异常,那么就一定需要catch去捕捉这个异常,如果我的catch去捕捉编译时这个异常,那么我的try中一定要抛出这个异常,要有来有回,它们是共生的关系,不能分离!

    注意
    如果抛出的异常与 catch 时异常类型不匹配,即异常不会被成功捕获,也不会被处理,继续往外抛,直到 JVM收到后中断程序,异常是按类型来捕捉的!

    当然,如果我们对于异常的处理方式是一样的时,我们在catch中也可以这样写

    catch (NullPointerException | ArrayIndexOutOfBoundsException e){
    	//处理异常
    }
    

    那么既然数组越界异常和空指针异常的父类都是 Exception 类,那么能不能直接一次性捕获多个异常呢?由于 Exception 类是所有异常类的父类,因此可以用这个类型表示捕捉所有异常,没有任何问题,但是不推荐!如果这样写的话,即使是捕捉到了异常也很难对该异常进行分辨,代码的可读性也会大大降低!

    	//一次性捕获多个异常
    catch (Exceptionn ex) {
        //处理异常
    }
    

    如果异常之间具有父子关系,一定是子类异常在前catch,父类异常在后catch,否则语法错误

    catch (NullPointerException e) {
               System.out.println("捕获到了空指针这个异常!!!");
           }catch (ArrayIndexOutOfBoundsException e) {
               System.out.println("抓到了数组越界这个异常");
               e.printStackTrace();
           }catch (Exceptionn ex) {
       	//处理异常
    	}
    

    原因:如果我们的父类在前,那么就代表这个异常永远都不会走到子类的catch里,从逻辑上讲,我们的catch(子类)就没有了用处,所以我们要将子类的catch写在前面,这样写的好处是,即使我的子类没有捕捉到这个异常,但是我的父类会帮助我们将遗漏的异常捕捉到。

    注意catch 进行类型匹配的时候, 不光会匹配相同类型的异常对象, 也会捕捉目标异常类型的子类对象

    🍄finally 关键字

    我们来看这段代码

    public static void main(String[] args) {
            try {
                int a=10/0;
            }catch (NullPointerException e) {
                System.out.println("捕获到了空指针这个异常!!!");
            }catch (ArrayIndexOutOfBoundsException e) {
                System.out.println("抓到了数组越界这个异常");
                e.printStackTrace();
            }
            System.out.println("我是后续代码.....");
        }
    

    最后执行结果为

    原因是我们并没有捕捉到这个异常,所以最后交给了JVM处理,但是如果我想在程序终止前将“我是后续代码”这句话输入,有没有办法?
    答案是有的!我们可以通过关键词finally来进行处理。
    我们看修改之后的代码

    在被finally修饰的代码块中,我们可以顺利运行里面的内容!
    接下来真正介绍finally
    在写程序时,有些特定的代码,不论程序是否发生异常,都需要执行,比如程序中打开的资源:网络连接、数据库连接、IO流等,在程序正常或者异常退出时,必须要对资源进进行回收。另外,因为异常会引发程序的跳转,可能导致有些语句执行不到,finally就是用来解决这个问题的。

    语法格式
    try{
    //
    //将可能出现异常的代码放在这里
    //
    } catch(要捕获的异常类型 e){
    // 对异常就可以正常处理,处理完成后,跳出try-catch结构,继续执行后序代码
    }finally {
    //此处的语句无论是否发生异常,都会被执行到!!!
    }

    问题:既然 finally 和 try-catch-finally 后的代码都会执行,那为什么还要有finally呢?
    大家看下述代码

    public static int func() {
        Scanner scanner=new Scanner(System.in);
        try {
            int a=scanner.nextInt();
            return a;
        }catch (NullPointerException e) {
            System.out.println("捕获到了空指针这个异常!!!");
        }catch (ArrayIndexOutOfBoundsException e) {
            System.out.println("抓到了数组越界这个异常");
            e.printStackTrace();
        }
        System.out.println("try-catch-finally之后代码");
         scanner.close();
        return -1;
    }
    

    上述程序,如果正常输入,成功接收输入后程序就返回了,try-catch-finally之后的代码根本就没有执行,即输入流没有被释放,造成资源泄漏。
    但是如果使用finally

    public static int func() {
        Scanner scanner=new Scanner(System.in);
        try {
            int a=scanner.nextInt();
            return a;
        }catch (NullPointerException e) {
            System.out.println("捕获到了空指针这个异常!!!");
        }catch (ArrayIndexOutOfBoundsException e) {
            System.out.println("抓到了数组越界这个异常");
            e.printStackTrace();
        }
       finally {
            System.out.println("try-catch-finally之后代码");
            scanner.close();
        }
        return -1;
    }
    

    运行结果,执行了finally这个语句块,说明我们的输入流也已经释放成功了!

    注意finally中的代码一定会执行的,一般在finally中进行一些资源清理的扫尾工作

    我们再来看下面的这个代码,看看运行结果是什么?

    public static int func() {
        try  {
            int a=10;
            return a;
        } catch (NullPointerException e) {
            System.out.println("捕获到了空指针这个异常!!!");
            return 1;
        } catch (ArrayIndexOutOfBoundsException e) {
            System.out.println("抓到了数组越界这个异常");
            return 2;
        } finally {
            System.out.println("try-catch-finally之后代码");
            return 9;
        }
    }
        //结果是什么?
    

    运行结果

    总结

    ————————————————

  • finally 执行的时机是在方法返回之前(try 或者catch 中如果有 return,则会在这个 return 之前执行 finally).但是如果finally 中也存在 return 语句 , 那么就会执行 finally 中的 return, 从而不会执行到 try 中原有的return.
  • 一般我们不建议在 finally 中写 return (被编译器当做一个警告).
  • ————————————————

    🍃异常的处理流程


    【异常处理流程总结】

  • 程序先执行 try 中的代码
  • 如果 try 中的代码出现异常, 就会结束 try 中的代码, 看和 catch 中的异常类型是否匹配.
  • 如果找到匹配的异常类型, 就会执行 catch 中的代码
  • 如果没有找到匹配的异常类型, 就会将异常向上传递到上层调用者.
  • 无论是否找到匹配的异常类型, finally 中的代码都会被执行到(在该方法结束之前执行).
  • 如果上层调用者也没有处理的了异常, 就继续向上传递.
  • 一直到 main 方法也没有合适的代码处理异常, 就会交给 JVM 来进行处理, 此时程序就会异常终止.

  • 🍂自定义异常类

    虽然java中有好多提供给我们的异常类可以使用,但是还是不能够满足我们的需求,例如我们平时登录账号时,账号或者密码错误,我们想要这个程序终止运行,但是java中并没有这种异常类供我们使用,所以就需要我们自己去定义一些异常类来供自己使用!
    假设说这里我们模拟简单的分数判断功能,当分数大于100或小于0时,程序抛出异常!
    那么具体怎么自定义一个异常呢?

  • 自定义一个类,然后继承 Exception 或者 RuntimeException 类
  • 实现一个带有 String 类型参数的构造方法,参数也就是出现异常的原因
  • 这里我们就根据条件来自己实现一个异常类

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

    这里可以看到这个异常类我们继承了 RuntimeException 异常类,接着里面有一个构造方法,调用父类的构造传递了一个字符串,至于父类的实现我们目前可以不管,那么这样,就简单的自定义了一个异常类。
    接下来我们的问题就是如何抛出这个异常以及如何捕获这个异常!

      public static int func(int x) {
           if (x<0||x>100) {//当我的数大于100或小于0时抛出异常
               throw new FractionException("成绩错误");
           }
           else return x;
          }
        public static void main(String[] args) {
            Scanner scanner = new Scanner(System.in);
            try {
                System.out.println(func(scanner.nextInt()));//将返回的结果打印
            } catch (FractionException e) {//捕获这个异常
             e.printStackTrace();//打印这个异常
               System.out.println("成绩错误,请重新输入");
            }
            System.out.println("运行后续代码.....");
        } 
    

    当我们输入的是100时

    当我们输入的结果时-1时

    上述代码就是在func这个方法中进行判断,如果我输入的成绩正确,则返回输入的成绩,如果我输入的成绩不正确,则直接抛出成绩错误这个异常,并且由于在func这个方法中没有catch进行捕获,所以我们根据调用func方法的这个main方法中进行判断,由于main方法中有catch并且顺利处理了这个异常,我们的程序就可以继续执行!

    注意
    如果自定义异常类继承了 Exception类,默认是受检查异常(编译时异常)
    如果自定义异常类继承了 RuntimeException,默认是不受检查异常(运行时异常)

    物联沃分享整理
    物联沃-IOTWORD物联网 » 《Java:浅析异常》—— 适宜预习与复习

    发表评论