java异常与自定义异常

0 阅读10分钟

java自带的异常

  • 在java中,会有各种各样的异常,比如:在下方代码中创建了容器ints,但并未向容器中填入元素,我们直接向容器取值,这时候如果我们编译,java就会给我们报一个数组下标越界的异常
public class Main {  
    public static void getData(ArrayList ints){  
        ints.get(0); // 访问容器中的第一个元素  
    }  

    public static void main(String[] args) {  
        ArrayList ints = new ArrayList<>();  
        getData(ints); // 因为容器为空,所以调用该方法java会抛出数组下标越界异常  
        //Exception in thread "main" java.lang.IndexOutOfBoundsException  
    }  
}

image.png

  • 再如我们创建的ints2容器的引用,它只引用了一个空对象,我们直接向容器中取值,这时候java就会给我们报一个空指针异常。
public class Main {  
    public static void getData(ArrayList ints){  
        ints.get(0);// 访问容器中的第一个元素  
    }
    public static void main(String[] args) {  
        ArrayList ints = new ArrayList<>();  
        // 比如,我们创建一个空的容器对象  
        ArrayList ints2 = null;  
        getData(ints2); 
        // 因为我们的对象是空的,这时候java就会报出一个空指针异常  
        // Exception in thread "main" java.lang.NullPointerException  
        // 以上两个例子就是java自身自带的异常处理机制(也就是JRE帮我们捕捉到的异常))  
    }  
}

image.png

  • 这些异常实际上是在java的JVM中写好的,当JVM发现有不合法的行为,则会抛出异常,并终止运行

捕获异常

  • 当我们的代码中有不合法的操作的时候,如果我们编译运行代码,java的JVM会终止程序的运行,发生异常哪一行之后的代码都不在运行,有没有什么办法及处理异常,又不终止后续代码的运行呢?当然有,那就是使用try-catch-final捕获异常
    • 使用try捕获异常若异常捕获,try体内后续代码不再运行
    • catch需传入一个参数,该参数用于指定catch方法体内的代码用于处理那种异常
    • final体内的语句无论是否发生异常都会运行,通常在工程上在final体内通常会进行资源释放
  • 如下方代码,我们为可能发生的异常设计了两个catch,当异常发生既会提醒用户,还会继续运行try-catch-final之后的代码:
public class Main {  
    public static void getData(ArrayList ints){  
        try{  
            ints.get(0); // 访问容器中的第一个元素  
            //当try内的语句发生了异常,会直接跳到对应的catch,而不运行try中后续的语句  
            System.out.println("一但异常发生,我就不在运行了");  
        }catch(IndexOutOfBoundsException e){  
            //捕获数组下标越界的异常  
            System.out.println("你要访问的位置0在ints中不存在");  
        }catch(NullPointerException e){  
            //再设计一个捕捉到空指针异常的处理方式  
            System.out.println("发生了空指针异常");  
        }finally {  
            //在工程中我们通常会再finally语句中对资源进行释放,如数据库的连接等  
            System.out.println("无论如何我都会执行");  
        }  
        System.out.println("虽然发生了异常,但我还是运行了");  
    }  
    public static void main(String[] args) {  
        ArrayList ints = new ArrayList<>();  
        getData(ints);  
        ArrayList ints2 = null;  
        getData(ints2);  
    }  
}

主动抛出异常

  • 有些时候一些业务可能有些特殊要求,比如要求输入不能为负数,这时候我们就可以为程序设计功能当输入不正确的时候抛出异常提醒用户,如下方代码所示:

    • 我们先使用if语句来指定抛出异常的要求是i<0
    • 我们再使用new IllegalArgumentException(Sting value)创建了一个异常对象,并向其构造方法传入了一个String类型的参数,这个String参数通常用于提示用户抛出异常的原因
    • 之后我们又使用throw new 语句将异常抛出
    • 而在main函数中我们可以结合try-catch-final来调用该方法,当然也可以直接调用该方法不做处理(这样程序抛出异常则会停止运行,后续代码也不会执行)
    • 当我们捕获到我们主动抛出的异常时,可以使用e.getMessage()来查看抛出异常的信息,发现这个信息就是我们在创建抛出异常对象是传入的字符串
    public class Main {
        public static void check(int i){
            if (i < 0){
                //当i小于1的时候抛出参数不合法的异常
                throw new IllegalArgumentException("i<0,它是不合法的");
                //该异常需要传入一个字符串,传入的字符串通常用于提示用户抛出异常的原因
            }else {
                System.out.println("i是合法的");
            }
        }
        public static void main(String[] args) {
            try {
                check(-5);
            }catch(IllegalArgumentException e){
                System.out.println("i<0不合法的");
                System.out.println(e.getMessage());
                //使用getMessage方法可以直接查找我们指定的异常抛出提醒用户的信息
            }finally {
                System.out.println("try-catch运行结束");
            }
            System.out.println("虽然发生异常,但我运行了");
        }
    }
    

    image.png

  • 我们可以通过查看异常的原型来看下,java究竟是如何实现异常的。

    • 按住ctrl + 右键点击,查看IllegalArgumentException的原型,可以看到我们跳到了IllegalArgumentException类的构造方法中这个方法要求传入String类型的参数,除此之外一旁还有其他的构造方法用于重载,而IllegalArgumentException这是继承于RuntimeException类 image.png

    • 进入到RuntimeException之后可以看到匹配到了如下图所示的构造方法,带着message参数,通过super(message)又调用了父类Exception的构造方法 image.png

    • 进入到Exception类之后匹配到了如下图所示的构造方法,带着message参数,通过super(message)又调用了父类Throwable的构造方法 image.png

    • Throwable是匹配如下图所示的构造方法,并调用了fillInStackTrace()方法 image.png

      image.png

    • fillInStackTrace()方法在使用synchrnized锁保证线程安全的情况下,将异常抛出

    • 当使用e.getMessage()的时候实际上就是在获得detailMessage的值,这个值是通过super一层层传递过来的

    • 这样设计的目的无论自定义异常是怎样的,都会将异常信息和操作传递到父类并运行,本质上是继承了父类的一系列操作方法

    • `可以发现异常的本质就是一串继承于Throwable的类

  • 我们可以在方法的神明中加上throws 异常名的字样,来提醒调用该方法的开发者,该方法可能会抛出一个异常

public class Main {
    public static void check(int i) throws IllegalArgumentException{
        if (i < 0){
            //当i小于1的时候抛出参数不合法的异常
            throw new IllegalArgumentException("i<0,它是不合法的");
        }else {
            System.out.println("i是合法的");
        }
    }

    public static void main(String[] args) {
        check(-1);
    }

}
  • 这时候我们在调用调用方法,则IDE会提示我们这个方法会抛出一个异常,建议使用try-catch-final进行捕获 image.png
    • 一般来说,不是很严重的比如运行时候异常IDE会在鼠标放在方法上查看方法用法是进行提示
    • 而严重的,比如检查型异常则,方法体声明必须使用throw表示要抛出异常,且需要强制使用try-catch-final捕获异常或将异常抛给父类,如下方代码所示
    // 写法一:必须声明throws或在方法内try-catch
    public static void readFile() throws IOException {
        // 代码可能抛出IOException
        throw new IOException("文件不存在");
    }
    
    // 写法二:编译错误!必须处理受检异常
    public static void readFile() {
        throw new IOException("文件不存在");  // 编译错误:未报告的异常
    }
    
  • 有时候我们也会将异常抛出类,给下一个调用该类方法的类来处理
class DataAccessLayer {
    // 数据访问层,可能抛出SQLException
    public static void saveData(int data) throws Exception {
        // 模拟数据库操作
        if (data < 0) {
            throw new Exception("小于1,数据验证失败");
        }
        System.out.println("数据保存成功: " + data);
    }
}

public class ServiceLayer {
    // 服务层方法,不处理异常,继续向上抛出
    public static void processRequest(int data) throws Exception {
        // 调用数据访问层
        DataAccessLayer.saveData(data);
    }
}

public class Main {
    public static void main(String[] args) {
        try {
            // 最终在顶层处理所有异常
            ServiceLayer.processRequest(-5);
        } catch (Exception e) {
            System.out.println("顶层捕获异常: " + e.getMessage());
        }
    }
}

java异常的分类和类结构图

  • java的标准库内建了一些通用的异常,这些类以Throwable为顶层父类。Throwable又派生出Error类和Exception类。 exception-hierarchy.png
    • Error(错误)类:该类及其子类的实例,代表JVM本身错误。是程序无法恢复的异常情况,Error很少出现,更为需要关注的是Exception类及其子类的各种异常类。

    • Exception(异常)类:该类及其子类,代表程序运行时发生了各种不期望发生的事件。可以被Java异常处理机制使用,是异常处理的核心。

      • Exception类在Java API中,声明了几百个Exception的子类分别来代表各种各样的常见异常情况,这些类根据需要代表的情况位于不同的包中,这些类的类名均以 Exception作为类名的后缀。如果遇到的异常情况,Java API中没有对应的异常类进行代表,也可以声明新的异常类来代表特定的情况。在这些异常类中,根据是否是程序自身导致的异常,将所有的异常类分为两种:
        • RuntimeException及其所有子类:该类异常属于程序运行时异常,也就是由于程序自身的问题导致产生的异常,例如数组下标越界异常ArrayIndexOutOfBoundsException等。 该类异常在语法上不强制程序员必须处理,即使不处理这样的异常也不会出现语法错误。
        • 其它Exception子类:该类异常属于程序外部的问题引起的异常,也就是由于程序运行时某些外部问题导致产生的异常,例如文件不存在异常FileNotFoundException等。该类异常在语法上强制程序员必须进行处理,如果不进行处理则会出现语法错误。
        • 可以通过查文档的方式来了解各种有异常的信息:搜索到Exception之后点击use即可以查看父类Exception被哪些包中的哪些类使用,java.lang.exception 类的用途(Java SE 17 和 JDK 17) --- Uses of Class java.lang.Exception (Java SE 17 & JDK 17)

      image.png

自行定义异常

  • 除了java标准库中自带的异常,我们还可以自己定义异常

image.png

  • 如下方代码所示,我创建了一个叫StringLengthException的异常,用来限制传入的字符串长度,并且让其继承于IOException异常:
import java.io.IOException;
public class StringLengthException extends IOException {  
    //自己创建的字符超长异常继承自IO异常,IO异常是一个检查型异常  
    int length;
    // 创建一个构造方法,要求示例化异常对象的时候要传入length参数
    public StringLengthException(int length) {  
        this.length = length;  
    }  
    public int getLength() {  
        return length;  
    }  
}
  • 在main类中设计一个方法,这个方法在某些情况下会抛出我们的异常,比如字符串参数的长度小于5,因为我们的StringLengthException继承了IOException,IOException是一个检查型异常,java会强制我们将StringLengthException抛出,如下方代码所示:
public class Main {  
    public static void checkString() throws StringLengthException{  
        String str;  
        System.out.println("输入字符串");  
        Scanner scanner = new Scanner(System.in);  
        str = scanner.nextLine();  

        int length = str.length();  
        if (length < 5){  
            throw new StringLengthException(length);
            //我们在设计异常类的时候设计构造方法,强制要求传入一个int类型的参数  
        }else {  
            System.out.println("字符串的长度是:" + length);  
        }  
    }  
}
  • 要调用checkString方法Java会强制我们使用try-catch-final捕获异常,因为我们checkString方法抛出的StringLengthException继承了IOException,IOException是一个检查型异常,所以我们必须捕获或向外抛出这个异常,但因为在main方法中,无法再向外抛出了,所以就只能使用try-catch-final一个方案,如下方代码所示:
public class Main {  
    public static void main(String[] args) {  
        // checkString();  
        try{  
            checkString();  
        }catch (StringLengthException e){  
            // throw new RuntimeException();  
            System.out.println("当前这个字符串的长度是,太短了:" + e.getLength());  
        }  
    }  
}