java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Java Exception异常

详解Java的Exception异常机制

作者:fntp

Java的Exception异常机制,为什么会突然聊到异常?其实不是突然,而是我已经准备了很久,但苦于没有好的例子来讲解,从表象到底层实现,今天就带大家详细了解Exception异常,需要的朋友可以参考下

一、前言

在Java中,我们在执行代码的过程中难免会遇到错误与Exception异常,可是我们一直都是锤头Coding而忽略了学习Exception这个东西!我们只是知道在发生Exception的地方让代码自动生成throw exception或者是使用try-catch括起来处理,那你了解Java的Exception吗?今天就让我们把一起来看看Java的Exception吧!

在Java中,我们的代码再出现错误的时候无非是两种情况:一是Error,一是异常Exception。如果是Error,那么则说明出现了错误,错误出现在编译器预编译阶段,错误是可以预见的,而异常却是无法预见的。Java中,无论是Error还是Exception都是Throwable的子类(见下图)。这是个什么玩意儿,可能大部分的人都没人听说过吧…不过这并不重要…开始上图…

image-20210526093139626

Error与Exception的区别与相同点:

Error与Exception的相同点:

Exception类是所有异常的超类。

Java所有的异常在寻找父类的时候最终都能找到Exception类。

异常的分类

java.lang.Exception类是所有异常的超类,主要分为以下两种:

RuntimeException-运行时异常,也叫作非检测性异常.

lOException和其它异常-其它异常,也叫作检测性异常,所谓检测性异常就是指在编译阶段都能被编译器检测出来的异常。
其中RuntimeException类的主要子类︰ArithmeticException类-算术异常,ArraylndexOutOfBoundsException类-数组下标越界异常,NullPointerException-空指针异常,ClassCastException-类型转换异常,NumberEormatException-数字格式异常注意:
当程序执行过程中发生异常但又没有手动处理时,则由Java虚拟机采用默认方式处理异常,而默认处理方式就是︰打印异常的名称、异常发生的原因、异常发生的位置以及终止程序。

二、关于RuntimeException

非检测性异常

package com.sinsy.exception;

public class ExceptionTest {
    public static void main(String[] args) {
        // 非检测性异常 : Exception in thread "main" java.lang.ArithmeticException: / by zero jvm无法检测
        System.out.println(5 / 0);
        //检测型异常
        Thread.sleep(3000);
    }
}

image-20210526092615338

RuntimeException的主要子类有:

下面通过代码来对异常做出解释:

image-20210526093930938

为了让所有的异常都能正常的显示出来,我这里对每一种异常使用多线程来打印输出:

package com.sinsy.exception;
public class ExceptionTest {
    public static void main(String[] args) throws Exception {
        // 非检测性异常 : Exception in thread "main" java.lang.ArithmeticException: / by zero jvm无法检测
        // System.out.println(5 / 0);
        //检测型异常
        //Thread.sleep(3000);
        exceptionTest();
    }
    public static void exceptionTest(){
       new Thread(new Runnable() {
           @Override
           public void run() {
               //第一种 数组下标越界异常
               int arr[] = new int[5];
               System.out.println(arr[5]);
           }
       }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                //算数异常
                int a = 1;
                int b = 0;
                System.out.println(a/b);
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                //空指针异常
                String abc = null;
                System.out.println(abc.length());
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                //类型转换异常
                Exception exception = new Exception();
                InterruptedException interruptedException =(InterruptedException) exception;
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                //数字格式异常
                String ad = "1234a";
                System.out.println(Integer.parseInt(ad));
            }
        }).start();
    }
}

运行之后的结果为:很清晰的看见,每一个线程代表着一种异常。

image-20210526094611848

通过编码对各种异常进行处理之后,结果为:

/**
 * 测试各种异常转换机制的修复
 */
public static void exceptionModifyTest(){
    new Thread(() -> {
        //第一种 数组下标越界异常
        int arr[] = new int[5];
        if (arr.length>5){
        System.out.println(arr[5]);
        }
        System.out.println("下标越界异常已经规避");
    }).start();
    new Thread(new Runnable() {
        @Override
        public void run() {
            //算数异常
        int a = 1;
        int b = 0;
        if (0!=b){
        System.out.println(a/b);
        }
            System.out.println("算术异常已经规避");
        }
    }).start();
    new Thread(() -> {
        //空指针异常
        String abc = null;
        if (null!=abc) {
            System.out.println(abc.length());
        }
        System.out.println("空指针异常已经规避");
    }).start();
    new Thread(() -> {
        //类型转换异常
        Exception exception = new Exception();
        if (exception instanceof  InterruptedException) {
            InterruptedException interruptedException = (InterruptedException) exception;
        }
        System.out.println("类型转换异常已经规避");
    }).start();
    new Thread(() -> {
        //数字格式异常
        String ad = "1234a";
        //字符串类型的数据做筛选一般选择正则表达式做第一选择
        if (ad.matches("\\d+")){
        System.out.println(Integer.parseInt(ad));
        }
        System.out.println("数字格式异常已避免");
    }).start();
}

运行结果为:

image-20210526100042807

三、异常的避免

异常的避免其实上文已经凸显出来了结局方案,就是在可能出现异常的地方进行if判断处理,提前预判异常,然后对可能出现的异常跳过处理。

异常的避免(使用If-else)可能会导致大量的代码冗余,导致代码的沉积度过大,变得臃肿,可读性比较差。

四、异常的捕获

异常的捕获使用try{}catch{}来进行捕获

捕获不了的我们对其进行抛出

五、异常的抛出(异常的转移)

异常抛出抛给谁呢?当然是异常的捕获者。

异常抛出的时候需要注意一点,抛出的异常之间有父子类继承关系的,如果跑的异常包含多个,那么我们可以选择抛出异常最大的父类,直接可以抛出所有异常,这样节省了代码,但同时也对代码的可读性造成了一定的影响,让修改者无法直接得知此代码在使用过程中会出现什么样的异常。

基本概念

在某些特殊情况下有些异常不能处理或者不便于处理时,就可以将该异常转秘给该方法的调用者,这种方法就叫异常的抛出,本质上其实是异常的转移。方法执行时出现异常,则底层生成一个异常类对象抛出,此时异常代码后续的代码就不再执行。

语法格式

访问权限返回值类型方法名称(形参列表) throws异常类型1,异常类型2…{方法体;}如︰
public void show() throws lOExceptiont

方法重写的原则

a.要求方法名相同、参数列表相同以及返回值类型相同,从jdk1.5开始支持返回子类类型;b.要求方法的访问权限不能变小,可以相同或者变大;
c.要求方法不能抛出更大的异常;

注意

子类重写的方法不能抛出更大的异常、不能抛出平级不一样的异常,但可以抛出一样的异常、更小的异常以及不抛出异常。一定注意!!!!!!!!!!若父类中被重写的方法没有抛出异常时,则子类中重写的方法只能进行异常的捕获。不建议在Main方法中抛出异常,JVM需要执行的任务有很多,如果此时将大量的异常工作交给他来做,那么会影响JVMd额执行效率

异常的常规处理

对于常见的异常,通常对不同类型的异常有着不同的处理方法.

(1)如果父类方法抛出了异常,子类不能抛出比父类更大的异常,只能抛出与父类所抛异常类子类的异常,特殊情况可不抛出任何异常。

/**
 * 父类
 */
package com.sinsy.test;
import java.io.FileInputStream;
import java.io.IOException;
public class ThrowExceptionFather {
    public  void show() throws IOException, InterruptedException {
        Thread.sleep(100);
        System.out.println("hehehe");
        FileInputStream fileInputStream =new FileInputStream("");
        fileInputStream.close();
    }
}
/**
 * 子类	
 */
package com.sinsy.test;
public class ThrowExceptionTest extends ThrowExceptionFather{
    @Override
    public void show() throws InterruptedException {
        Thread.sleep(20);
    }
}
/**
 * 父类
 */
package com.sinsy.test;
import java.io.FileInputStream;
import java.io.IOException;
public class ThrowExceptionFather {
    public  void show() throws IOException {
        FileInputStream fileInputStream =new FileInputStream("");
        fileInputStream.close();
    }
}

/**
 * 子类
 */
package com.sinsy.test;
public class ThrowExceptionTest extends ThrowExceptionFather{
    @Override
    public void show() throws InterruptedException {
        Thread.sleep(20);
    }
}

此时代码报错,证明子类无法抛出与父类非同级关系的异常:

image-20210526153413171

/**
 * 父类
 */
package com.sinsy.test;
import com.sun.jdi.ClassNotLoadedException;
import java.io.FileInputStream;
import java.io.IOException;
public class ThrowExceptionFather {
    public  void show() throws IOException {
        FileInputStream fileInputStream =new FileInputStream("");
        fileInputStream.close();
    }
}

/**
 * 子类
 */
package com.sinsy.test;
import com.sun.jdi.ClassNotLoadedException;
public class ThrowExceptionTest extends ThrowExceptionFather{
    @Override
    public void show() throws ClassNotLoadedException {
    }
}

image-20210526153935075

但是子类可以抛出一个是父类所抛异常类的子类的异常:

image-20210526154434907

通过对比IOException的简单继承关系我们可以选择一个IOException的子类,让继承了ThrowExceptionFather的子类在方法重写的时候去抛出该异常,如下代码:

package com.sinsy.test;

import com.sun.jdi.ClassNotLoadedException;

import java.nio.file.FileSystemException;

public class ThrowExceptionTest extends ThrowExceptionFather{
    @Override
    public void show() throws FileSystemException {

    }
}

此时未报错:

image-20210526154521219

证明,子类可以抛出父类所抛异常类子类的异常。

因此,总结一句话就是:子类继承父类后,在重写父类方法的时候,如果父类中原有方法抛出了异常,那么子类不能抛出比父类所抛异常大的异常,只能抛出属于父类所抛异常类的子类的异常。

(2)若一个方法内部以递进式的方法分别调用了好几个其他的方法,则建议这些方法将异常逐层抛出,然后再统一处理。如下代码所示。

package com.sinsy.test;
public class DigonThrow {
    public static void yu() {
        try {
            yu1();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    public static void yu1() throws Exception {
        yu2();
    }
    public static void yu2() throws Exception {
        yu3();
    }
    public static void yu3() throws Exception {
        yu4();
    }
    public static void yu4() throws Exception {
      yu5();
    }
    public static void yu5()throws Exception{
        System.out.println(1);
    }
    public static void main(String[] args){
        yu();
    }
}

将异常抛至最顶层的时候,此时做最后的 try-catch 处理。

(3)如果父类中被重写的方法没有抛出异常的时候,则子类中重写的方法只能进行异常的捕获处理。

//父类代码
package com.sinsy.test;
public class ThrowExceptionFather {
    public  void show(){

    }
}
//子类代码
package com.sinsy.test;
import java.io.FileInputStream;
import java.io.IOException;

public class ThrowExceptionTest extends ThrowExceptionFather{
    @Override
    public void show() {
        FileInputStream fileInputStream =new FileInputStream("");
        fileInputStream.close();
    }
}

若此时子类抛出异常,则会出现以下警示:

Method ‘show' is inherited.Do you want to add exceptions to method signatures in the whole method hierarchy?意思就是在说,方法‘Show'是继承的。要在整个方法层次结构中向方法签名添加异常吗?如果添加了之后,则父类中过就会抛出此异常!

image-20210526160825717

六、自定义异常

基本概念

当需要在程序中表达年龄不合理的情况时,而Java官方又没有提供这种针对性的异常,此时就需要程序员自定义异常加以描述。

实现流程

a.自定义xxxException异常类继承Exception类或者其子类。
b.提供两个版本的构造方法,一个是在·无参构造方法,另外一个是字符电作为参数的构造方法。

异常的产生

throw new异常类型(实参);如∶throw new AgeException("年龄不合理!!! ");Java采用的异常处理机制,是将异常处理的程序代码集中在一起,与正常的程序代码分开**,使得程序简洁、优雅,并易于维护。**

自定义Exception异常类

要想实现自定义Exception异常类,我们需要写一个Exception的异常类去继承Exception类,实现无参构造方法以及有参数构造方法。

package com.sinsy.exception;

public class AgeException extends Exception{
    
    static final long serialVersionUID = -3387516993124229948L;

    /**
     * 无参数构造方法
     */
    public AgeException() {
    }

    /**
     * 有参构造方法
     * @param message
     */
    public AgeException(String message) {
        super(message);
    }
}

(1)首先确定应用场景为:用户个人信息的封装中,对成员属性的封装的时候对成员属性的设置出现异常:

package com.sinsy.bean;
import com.sinsy.exception.AgeException;
import java.util.Objects;
public class Person {
    private String name;
    private String sex;
    private int age;
    public Person() {
    }
    public Person(String name, String sex, int age) {
        this.name = name;
        this.sex = sex;
        this.age = age;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getSex() {
        return sex;
    }
    public void setSex(String sex) {
        this.sex = sex;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) throws AgeException {
        if (age<0){
            throw new AgeException("年龄设置错误哦!请稍后重试!");
        }else{
            this.age = age;
        }
    }
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return age == person.age && Objects.equals(name, person.name) && Objects.equals(sex, person.sex);
    }
    @Override
    public int hashCode() {
        return Objects.hash(name, sex, age);
    }
    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", sex='" + sex + '\'' +
                ", age=" + age +
                '}';
    }
}

(2)确定引用场景之后,我们创建Test测试方法对目标用例进行简单测试:

package com.sinsy.test;

import com.sinsy.bean.Person;
import com.sinsy.exception.AgeException;

public class PersonException {
    public static void main(String[] args) {
        Person person = new Person("二狗子");
        try {
            person.setAge(-12);
        }catch (AgeException ageException){
            ageException.printStackTrace();
        }
        System.out.println("执行完毕");
    }
}

测试结果如下:

image-20210526185018926

这表明,AgeException的异常类使用正确,正确的打印出来了错误提示信息,这里的错误提示信息是自己自定义的,也就是你在调用AgeException类的时候自己输入的异常提示信息。这就是调用了AgeExceptiuon类的默认含参数的构造方法。

注意含有异常处理的代码执行顺序:

在遇到异常的时候我们需要根据异常出现的具体应用场景作相应的处理,如果是重写父类的继承下来的方法,我们在对方法进行重写的时候,我们需要注意,如果父类的原始方法并没有抛出异常,在子类中重写父类的方法是不需要抛出异常的,否则则会报错,而在处理异常的时候,我们更需要关注的是异常出现的位置以及时机。如果异常出现在可预测区域,则主动应该抛出对应可能会出现的异常,让代码安全的执行,不产生额外的错误。其二,在处理异常的时候,如果是递进式抛出异常,在最顶层的时候一定要try-catch处理异常,不可再向上抛出,如果抛给虚拟机来处理,那么则会响应JVM的执行效率。其三,当处理异常的时候,如果我们遇到与异常耦合度不高的业务需要执行的时候,我们可以在finally中编写与异常无耦合度的代码,确保必要的有业务能正常执行。

处理异常的时候由于finally与try-catch的关系,有时候会打乱原有代码的执行顺序,也会间接影响业务的执行:比如以下处理方法的不同导致在业务最后的结果完全不一致。代码如下

package com.sinsy.test;
import com.sinsy.bean.Person;
import com.sinsy.exception.AgeException;
public class PersonException {
    public static void main(String[] args) {
        Person person = new Person("二狗子");
        //注意我这里的处理异常的方式方法: 是在最顶层拿到异常之后直接当场做的处理,
        try {
            person.setAge(-12);
        }catch (AgeException ageException){
            //这里就是处理 直接将异常打印输出
            ageException.printStackTrace();
        }
        System.out.println("执行完毕");
    }
}

处理之后的结果为:

image-20210526191103339

**而如果我们将代码做一个简单的修改:**在设置年龄的2时候我们不对错误年龄进行抛出异常处理而是直接就地处理。则会怎么样呢?为了让效果更加明显,我们将SetAge的方法修改完之后我们再继续将测试方法中测试用例做一个简单的更改:

public void setAge(int age) {
    if (age<0){
       try {
           throw new AgeException("年龄设置错误哦!请稍后重试!");
       }catch (AgeException ageException){
           ageException.printStackTrace();
       }
    }else{
        this.age = age;
    }
}

//测试方法:
package com.sinsy.test;
import com.sinsy.bean.Person;
import com.sinsy.exception.AgeException;
public class PersonException {
    public static void main(String[] args) {
        Person person = new Person("二狗子","男");
        //注意我这里的处理异常的方式方法: 是在最顶层拿到异常之后直接当场做的处理,
        person.setAge(-12);
        System.out.println(person);
    }
}

最后的处理结果:

image-20210526192743433

我们发现,测试方法执行之后,打印输出了Person对象person。

可见,在设置年龄的时候就地处理,不会影响后续代码的执行,就地处理后,我们捕获到异常,怎么处理异常取决于我们对异常的动作,我们可以选择将捕获到的异常进行打印亦或是将异常转交到其他处理异常的业务中去处理,这样的做了就地处理不会影响我们后续的代码执行。

好了,以上就是今天对Java异常的介绍与使用以及教学,下一篇我们就来研究一下Java中的异常的底层是怎么实现的,Exception在Java中是一种什么样的存在?设计Exception与Error的目的在JVM处理字节码文件指令集的过程中能起到什么样的效果?

说到这里,如果你有一点基础的话,相信肯定能看到我对作用(设计Exception与Error的目的在JVM处理字节码文件指令集的过程中能起到的作用)的一些简单了解,没事,不够清晰,我们还有第二篇博客详解Exception与Error。

到此这篇关于详解Java的Exception异常机制的文章就介绍到这了,更多相关Java Exception异常内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文