08、异常处理


前言

      去年四月份大一下半学期正式开始学习Java,一路从java基础、数据库、jdbc、javaweb、ssm以及Springboot,其中也学习了一段时间数据结构。

      在javaweb期间做了图书商城项目、ssm阶段做了权限管理项目,springboot学了之后手痒去b站看视频做了个个人博客项目(已部署到服务器,正在备案中)。期间也不断进行做笔记,总结,但是越学到后面越感觉有点虚,觉得自己基础还有欠缺。

      之后一段时间我会重新回顾java基础、学习一些设计模式,学习多线程并发之类,以及接触一些jvm的相关知识,越学到后面越会感觉到基础的重要性,之后也会以博客形式输出学习的内容。

      现在整理的java知识基础点是在之前学习尚硅谷java课程的笔记基础之上加工汇总,部分图片会引用尚硅谷或网络上搜集或自己画,在重新回顾的过程中也在不断进行查漏补缺,尽可能将之前困惑的点都解决,让自己更上一层楼吧。

      博客目录索引博客目录索引(持续更新)



一、异常概述与异常体系结构

概述说明

异常分类:编译时异常与运行时异常

  • 编译时异常:源代码.java文件在编译器编译时发生的错误。
  • 运行时异常:执行字节码文件时发生不正确情况,其称为异常。

这里要讲的异常指的是运行时异常,其分为两类异常事件

  • Error:Java虚拟机无法解决的严重问题。例如:JVM系统内部错误、资源耗尽等严重情况。比如:StackOverflowError(栈溢出)和OOM(内存溢出)。
  • Exception:其他因编程错误或偶然的外在因素导致的一般性问题,可以使用针对性的代码进行处理,例如空指针访问、试图读取不存在文件、网络连接中断、数组角标越界问题。

Error表示不可控异常一般不编写针对性的代码进行处理;而对于Exception这类异常,我们可以编写针对性的代码进行处理。


Error的实例

看Error的两个实例:栈溢出与内存溢出

package com.mjj.pro7;

public class Test1 {
    public static void main(String[] args) {
        //1.栈溢出,无限压栈 报错 java.lang.StackOverflowError
        //main(args);
        //2.堆溢出,创建占用超过初始jvm使用的内存,java.lang.OutOfMemoryError:
        Integer[] arr = new Integer[1024*1024*1024];
    }
}

针对于这Error的情况,我们不对其进行针对性处理。



二、常见异常

异常体系结构

非受检异常:例如RuntimeException在默认情况下会得到自动处理,可以捕获RuntimeException异常,但对于自己的封装RuntimeException的异常,一部分还是需要进行手动抛出。

受检异常:Java编译器要求程序必须捕获或声明抛出这种异常。



RuntimeException举例

表示运行时异常,接下来进行实例举例

NullPointerException(空指针)

import org.junit.Test;

public class ExceptionTest {

    //NullPointerException
    @Test
    public void test1(){
        //例1
//      int[] arr = null;
//      System.out.println(arr[3]);
        
        //例2
        String str = null;
        System.out.println(str.charAt(0));
    }
}


IndexOutOfBoundsException(下标越界)

//IndexOutOfBoundsException
    @Test
    public void test2(){
        //第一种 ArraryIndexOutOfBoundsException
//      int[] arr = new int[10];
//      System.out.println(arr[10]);
        
        //第二种 StringIndexOutOfBoundsException
        String arr = "123";
        System.out.println(arr.charAt(3));
    }


ClassCastException(类型转换)

//ClassCastException 类型转换问题
@Test
public void test3(){
    Object obj = new Date();
    String str = (String)obj;
}


NumberFormatException(数值转换)

//NumberFormatException 数值类型转换
@Test
public void test4(){
    String str = "123";  //是通过的
    String str1 = "abc";
    int num = Integer.parseInt(str1);
}


InputMismatchException(输入不匹配)

//InputMismatchException 输入不匹配
@Test
public void test5(){
    Scanner sca = new Scanner(System.in);
    int num = sca.nextInt();  //当输入abc时会报这个错误
    sca.close();
}


ArithmeticException(算术异常)

//ArithmeticException 算术异常
@Test
public void test6(){
    System.out.println(5/0);//java.lang.ArithmeticException: / by zero
}


三、异常处理概述

异常处理好处

:对于上面异常体系结构中不受检异常指的是我们不进行异常处理系统也会自行捕捉到异常,并且输出异常信息。那么我们处理异常与不处理异常的区别在哪以及为什么要进行异常处理?

  • 区别描述:对不受检异常不进行异常处理时,若我们程序发生异常,就会直接终止程序;若是进行异常处理,程序会按照我们要求进行异常处理并且继续执行程序。
  • 目的:能够让我们对异常更好的处理以及程序继续执行下去。

看一下是否进行异常处理的区别

①不进行异常处理

public class Main {
    public static void main(String[] args){
        String str = "abc";
        int number;
        //有异常的语句
        int i = Integer.parseInt(str);
        System.out.println(123);
    }
}

image-20210128195644491

可以看到一旦出现异常程序直接停止,后面的语句不再执行!!!

②进行异常处理

public class Main {
    public static void main(String[] args){
        String str = "abc";
        int number;
        //进行异常处理
        try {
            int i = Integer.parseInt(str);
        } catch (NumberFormatException e) {
            e.printStackTrace();
        }
        System.out.println(123);
    }
}

image-20210128195826541

我们可以看到程序完整的执行了下来后面的语句也进行了执行,这里我们是对异常情况进行打印输出!!!



抓抛模型

对异常的处理会有两个过程:抛、抓

  • 过程一:"抛",程序在正常执行的过程中,一旦出现异常,就会在异常代码处生成一个对应异常类的对象,并将此对象抛出。一旦抛出对象以后,其后的代码都不执行。
  • 过程二:"抓",可以理解为对异常的处理方式,如:try-catch-finallythrows


异常处理机制一:try-catch-finally

语法

try{
//可能出现异常的代码
}catch(异常类型1 变量名1){
//处理异常的方式1
}catch(异常类型2 变量名2){
//处理异常的方式2
}
...
finally{
//一定会执行的代码
}

try-catch注意点

  1. try中包裹可能出现异常的代码,若出现异常先生成指定异常,接着去catch中去找匹配异常。
  2. 当try中出现错误,进入catch部分中进行异常处理,一旦处理完就执行finally中包裹内容(finally存在),接着跳出try-catch结构,继续执行try-catch结构之外的代码。
  3. catch中的异常类型如果没有子父类关系,谁在上谁在下都无所谓;若是多个catch中有子父类关系的,子类必须要声明在父类异常之上,否则报错。父类在上的话,子类异常声明在下就没意义了!!!
  4. catch的{}中异常对象处理方式常见的两种:
    • e.getMessage():获取异常的简略信息。
    • e.printStackTrace():比较常用,打印完整的堆栈情况,会显示指定的出错位置。
  5. 注意在try中声明的变量(局部变量),其生命周期只在try结构中,try-catch结构之外无法调用。

注意点5中的实例演示:

public class Main {
    public static void main(String[] args){
        String str = "abc";
        int number;
        try {
            int i = Integer.parseInt(str);
        } catch (NumberFormatException e) {
            //e.printStackTrace(); //详细堆栈错误信息
            System.out.println(e.getMessage());//简略信息
        }
        
    }
}

image-20210128194507363

image-20210128194559185



finally注意点

  1. finally中声明的代码是一定执行的,尽管catch中出现异常会先执行finally,再抛出异常。
  2. try-catch-finally使用于方法中catch与finally包含return,无论catch是否有异常,都会执行finally中的return返回

注意点1中情况举例

public class Main {
    public static void main(String[] args){
        try{
            int a=10;
            int b=0;
            System.out.println(a/b);
        }catch(ArithmeticException e){
            int[] a = new int[10];
            //这里有数组越界异常
            System.out.println(a[10]);
        }
      finally{
          System.out.println("执行finally语句");
      }
        System.out.println("长路哥哥");
    }
}
  • catch中如果出现异常,只会执行finally中的代码,try-catch结构外的也不会处理!!!

image-20210128200901666


注意点2中举例

public class Main {
    public static void main(String[] args){
        System.out.println(Main.method());
        System.out.println(123456);
    }

    public static int method(){
        try{
            System.out.println(10/0);
        }catch(ArithmeticException e){
            int[] a = new int[10];
            //这里有数组越界异常
            System.out.println(a[10]);
            return 2;
        }
        finally{
            return 3;
        }
    }
}

image-20210128202359796

可以看到结果没有出现异常,说明返回的是finally中的。

原因:在方法中catch出现异常了,会直接先去执行finally中内容,这里finally中是返回值,那么当catch异常处理前方法已经结束了,所以没有报异常出来!!!



finally实际使用

例如数据库连接、输入输出流,网络编程Socket等资源,JVM是不能自动的回收的,我们需要自己手动的进行资源的释放,此时的资源释放,就需要声明在finally中。

下面例子是演示输入输出流的关闭

import java.io.*;

public class Main {
    public static void main(String[] args){
        File file = new File("hello.txt");
        FileInputStream fis = null;
        try {
            fis = new FileInputStream(file);
            int read = fis.read();
            while(read != -1){
                System.out.println((char)read);
                read = fis.read();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            //fis放在这里进行关闭资源 再使用一个try-catch是因为IOException是受检型的必须声明
            try {
                if(fis != null)
                    fis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

}

在项目工程目录下添加hello.txt文件后执行

image-20210128212349704



总结

使用于main()中
try-catch:try异常,catch无异常进行处理,执行try-catch结构外的。
try-catch:try异常,catch异常,直接结束程序(打印异常)。
try-catch-finally:try异常,执行指定catch中,无异常会先执行完catch内容,接着执行finally中内容,然后执行try-catch以外内容。
try-catch-finally:try异常,执行指定catch中若是有异常会先执行finally内容,接着程序结束(打印异常)。

调用单独方法中
try-catch:try异常,catch无异常,正常执行后序程序。
try-catch:try异常,catch异常,程序直接结束(打印异常)。
try-catch-finally:catch与finally中都有返回值,若是catch中出现异常,会先去找finally,之后直接结束方法,此时异常也不会打印。
无try-catch结构捕捉异常,也无throws抛出,对于出现非受检查的异常系统会自动抛出。

总结总结:catch中无异常,执行完catch后执行finally以及try-catch结构之外的;catch中若是出现异常会先去执行finally中内容,接着程序直接停止。



异常处理机制二:throws

认识及使用throws

语法:throws 异常类

//例如
public void method()throws RuntimeException{
    
}

写在方法的声明处,指明此方法执行时可能会抛出的异常。一旦方法体执行时,出现异常,仍会在异常代码处生成一个异常类的对象,若此对象满足throws的异常就会抛出,在方法中出现异常代码后续的代码就不会执行。

与try-catch-finally比较

  • try-catch-finally:真正将异常处理掉。
  • throws:将异常抛给方法的调用者,并没有真正将异常处理掉。

实例1:throws声明可能会抛出的异常(表示了一种规范),方法中出现异常则会向上传递:

import java.io.*;

public class Main {
    public static void main(String[] args){
        try {
            method1();
        } catch (RuntimeException e) {
            System.out.println("捕捉到方法中的异常");
        }
    }

    public static void method1() throws RuntimeException {
        method2();
    }

    public static void method2 () throws RuntimeException{
        System.out.println(5/0);
    }
}

image-20210128223719226

其实就算我两个方法都不添加throws RuntimeException,最终使用try-catch也是能够接收到的,那为什么要使用thorows声明勒?也可以算是一种规范吧,声明则指明其方法可能会出现的异常,好让调用方法者知道捕捉异常时使用什么类型捕捉!!!不声明的话使用try-catch结构中catch默认会是Exception。



重写方法异常抛出规则

对于重写方法throws的规则

  • 子类重写方法的抛出异常类型应当小于或等于父类方法抛出异常类型
  • 父类中没有throws异常,子类重写时也不应该有

实例如下:

class SuperClass{

    public void method()throws IOException{

    }
}

class SubClass extends SuperClass{   //继承SuperClass
    @Override
    public void method() throws FileNotFoundException {  //子类重写方法异常应当小于等于父类的异常

    }
}


开发中如何选择异常处理机制

  1. 若是被重写父类的方法中没有throws,那么子类重写的方法必定也没有throws,若重写方法有异常,必须使用try-catch解决。
  2. 若调用多个不同的方法,并且几个方法是递进关系,那么建议使用throws,最先执行调用的方法使用try-catch捕捉异常。

try(){}语法

Java7 build 105版开始,Java7的编译器和运行环境支持新的 try-with-resources 语句,称为 ARM 块(Automatic Resource Management) ,自动资源管理。

语法如:try块退出时,会自动调用res.close()方法,关闭资源

try(Resource res = xxx)//可指定多个资源
{
     work with res
} 

这相比我们之前在finally中一个个手动关闭资源好的多。

我们看一下两者对比

//以前try{}catch{}:
FileInputStream fis = null;
FileOutputStream fos = null;
try {
    //目标图片1234.jpg
    fis = new FileInputStream(new File("1234.jpg"));
    //复制地址
    fos = new FileOutputStream(new File("图片.jpg"));
    ....
} catch (IOException e) {
    e.printStackTrace();
}finally {
    if(fis != null){
        try {
            fis.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    if(fos != null){
        try {
            fos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

//现在的try(){}
try (
    FileInputStream fis = new FileInputStream(new File("1234.jpg"));
    FileOutputStream fos = new FileOutputStream(new File("图片.jpg"));
	){
    ....
} catch (IOException e) {
    e.printStackTrace();
}

省去了大量的语句,舒服!!!



四、手动抛出异常throw

介绍一下异常对象的产生:①系统在出现异常时自动生成异常对象②手动生成一个异常对象,例如throw new Exception()

只要是使用了throw,就一定表示抛出了一个异常,而throws只是用于声明可能抛出的异常便于调用其方法的人做出相应的异常处理!!

实例:手动抛出异常,并使用throws声明该方法可能会有什么异常

public class Main {
    public static void main(String[] args){
        try {
            Main.method("");
        } catch (RuntimeException e) {
            System.out.println(e.getMessage());
        }
    }

    public static void method(String str)throws RuntimeException{
        if(!"".equals(str)){
            System.out.println(str);
        }else {
            throw new RuntimeException("str不能为空");
        }
    }

}

image-20210128233116965



五、自定义异常类

首先问一下为什么要自定义异常类勒?有几点原因,例如设置多个自己定义的异常类,仅仅捕获其自己所关心的异常类,并知道如何处理;根据自己的需求出现异常的时候来去做特殊的处理;异常分类,业务中不同场景抛出不同异常,便于统一捕获并根据类型做进一步处理

对于自定义异常类有几点规范如下

  • 继承于现有的异常类如:Exception、RuntimeException....
  • 自定义异常类需要提供一个全局常量如:serialVersionUID ,用来标识自己定义的异常类
  • 必须提供重载的构造器

自定义异常:包含最基本的几个部分

class MyException extends RuntimeException{
    //需要一个UID来表示自己的自定义异常类
    static final long serialVersionUID = -7034897190745766959L;

    public MyException(){
    }

    //想要有自定义异常描述,就需要有一个有参构造
    public MyException(String msg){
        super(msg);
    }
}
//测试上面的自定义异常
public class Main {
    public static void main(String[] args){
        try {
            Main.method("");
        } catch (MyException e) {
            System.out.println(e.getMessage());
        }
    }

	//测试使用
    public static void method(String str)throws MyException{
        if(!"".equals(str)){
            System.out.println(str);
        }else {
            throw new MyException("自定义异常描述:str不准为空");
        }
    }

}

image-20210128235408009



参考资料

[1]. Java受检异常和非受检异常

[2]. Java基础之《受检查异常和不受检查异常》

[3]. Java 中的异常和处理详解

[4]. Java中关键字throw和throws的区别

[5]. Java:为什么要有自定义异常?

[6]. Java自定义异常(优雅的处理异常) 实际应用配合枚举

[7]. java try(){}catch(){}自动资源释放



我是长路,感谢你的阅读,如有问题请指出,我会听取建议并进行修正。
欢迎关注我的公众号:长路Java,其中会包含软件安装等其他一些资料,包含一些视频教程以及学习路径分享。
学习讨论qq群:891507813 我们可以一起探讨学习
注明:转载可,需要附带上文章链接

原文地址:https://www.cnblogs.com/changluya/p/14423033.html