java认识异常精讲

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


一.异常的背景

1.1初识异常

我们在学习编程的过程中经常会遇到一些异常,举个例子:
除以0

System.out.println(10 / 0);
// 执行结果
Exception in thread "main" java.lang.ArithmeticException: / by zero

数组下标越界

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

访问 null 对象

public class Test {
    
    
    public int num = 10;
    public static void main(String[] args) {
    
    
        Test t = null;
        System.out.println(t.num);
   }
}
// 执行结果
Exception in thread "main".java.lang.NullPointerException

等等异常。。。
所谓的异常,也就是程序在运行时,出现错误通知调用者的一种机制
ps:关于“运行时”,有些错误是编译过程中就会报错,比如你return写成了retrun。运行时则是指程序已经得到class文件,再由JVM执行,在执行过程中产生的错误

异常的种类很多,不同种类的异常具有不同的含义,也有不同的处理方式。

1.2防御式编程

错误在代码中是客观存在的,因为我们要让程序出现问题时即使告知程序员,我们有两种主要方式:
1)LBYL(look before you leap):在操作之前就做充分的检查
2)EAFP(it is easier to ask for fogiveness than permission):“事后获取原来比事前获取许可更容易”,也就是先操作,遇到问题再处理
举个生动形象的例子:
你和你女朋友刚认识那会:
a)你:我可以牵你手吗?女朋友:好的
这种就是LBYL
b)你直接霸王硬上弓拉她手,她不同意你就事后做些补偿,同意了你就拉上了。
这种就是EAFP

1.3异常的好处

我们用一段伪代码来演示一下开一局游戏的过程

LBYL风格的代码(不使用异常)

boolean ret = false;
ret = 登陆游戏();
if (!ret) {
    
    
 处理登陆游戏错误;
    return;
}
ret = 开始匹配();
if (!ret) {
    
    
 处理匹配错误;
    return;
}
ret = 游戏确认();
if (!ret) {
    
    
 处理游戏确认错误;
    return;
}
ret = 选择英雄();
if (!ret) {
    
    
    处理选择英雄错误;
    return;
}
ret = 载入游戏画面();
if (!ret) {
    
    
 处理载入游戏错误;
    return;
}

EAFP风格的代码(使用异常)

try {
    
    //可能出错的都放进try里面,然后如果有错后面用catch捕捉其异常
    登陆游戏();
    开始匹配();
    游戏确认();
    选择英雄();
    载入游戏画面();
   ...
} catch (登陆游戏异常) {
    
    //catch用于捕捉异常
    处理登陆游戏异常;
} catch (开始匹配异常) {
    
    
 处理开始匹配异常;
} catch (游戏确认异常) {
    
    
 处理游戏确认异常;
} catch (选择英雄异常) {
    
    
 处理选择英雄异常;
} catch (载入游戏画面异常) {
    
    
 处理载入游戏画面异常;
}

对比两种不同风格的代码, 我们可以发现, 使用第一种方式, 正常流程和错误处理流程代码混在一起, 代码整体显的比较混乱. 而第二种方式正常流程和错误流程是分离开的, 更容易理解代码。

二、异常的基本用法

2.1捕捉异常

基本语法:

try{
    
     
 有可能出现异常的语句 ; 
 //可能有多个异常语句
}catch (异常类型 异常对象) {
    
    
} ... //可能不止一个catch
finally {
    
    
 异常的出口
}

try 代码块中放的是可能出现异常的代码.
catch 代码块中放的是出现异常后的处理行为.
finally 代码块中的代码用于处理善后工作, 会在最后执行.
其中 catch 和 finally 都可以根据情况选择加或者不加.

代码示例1.(不处理异常)

public class Test {
    
    
    public static void main(String[] args) {
    
    
        int []array={
    
    1,2,3};
        System.out.println(array[5]);//这里明显数组下标越界了
        System.out.println("hello");
    }
}

运行结果如下:
在这里插入图片描述
这里运行的话会给你一个数组越界异常,我们可以发现,如果它抛了异常,我们异常下面的代码就无法运行了,我们来试试处理这个异常
代码示例2.(使用 try catch 后的程序执行过程)

public class Test {
    
    
    public static void main(String[] args) {
    
    
        int []array={
    
    1,2,3};
        try{
    
    
            System.out.println(array[5]);
        }catch (ArrayIndexOutOfBoundsException e){
    
    
        //我们知道这里可能会抛出数组越界异常(上一张图片里就有)
        //ArrayIndexOutOfBoundsException是一个异常类,这个类后面我们可以创建一个变量
            System.out.println("已捕捉一个数组越界异常");
        }
        System.out.println("hello");
    }
}

运行结果如下:
在这里插入图片描述

需要注意的是, 一旦 try 中出现异常, 那么 try 代码块中的程序就不会继续执行, 而是交给 catch 中的代码来执行. catch 执行完毕会继续往下执行.
代码示例如下:

public class Test {
    
    
    public static void main(String[] args) {
    
    
        int []array={
    
    1,2,3};
        try{
    
    
            System.out.println(array[5]);
            System.out.println("haha");
        }catch (ArrayIndexOutOfBoundsException e){
    
    
            System.out.println("已捕捉一个数组越界异常");
        }
        System.out.println("hello");
    }
}

运行效果如下:
在这里插入图片描述
这里我们在try里面添加了 System.out.println(“haha”);但是因为它上一行代码抛出异常了,所以在try里面,这个异常下面的代码就不会执行了。出了try依然可以往下执行。

到这里我们也可以很直观的看出来,异常到底有什么作用,如果我们不处理异常,异常往下的代码都将无法执行,但如果我们处理异常,除了try里面的部分代码(从抛出异常那句代码开始)无法继续往下执行,但是出了try依然可以执行完整个进程,避免了因为一个异常耽误了其他程序运行。

有时候,我们需要定位异常的位置:

public class Test {
    
    
    public static void main(String[] args) {
    
    
        int []array={
    
    1,2,3};
        try{
    
    
            System.out.println(array[5]);
            System.out.println("haha");
        }catch (ArrayIndexOutOfBoundsException e){
    
    
            e.printStackTrace();//通过e打印栈上的信息
            System.out.println("已捕捉一个数组越界异常");
        }
        System.out.println("hello");
    }
}

在这里插入图片描述
我们可以通过异常类创建的对象e来打印栈上的信息(红色字体),然后系统会给你定位异常的位置,上图示为13行(蓝色字体),其他执行流程和前面的一样(一旦 try 中出现异常, 那么 try 代码块中的程序就不会继续执行, 而是交给 catch 中的代码来执行. catch 执行完毕会继续往下执行)

ps:如果我们不处理异常,那么这个异常会交给JVM处理,一旦交给JVM处理,程序会立即终止。

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

一些特殊情况

public class Test {
    
    
    public static void main(String[] args) {
    
    
        int []array={
    
    1,2,3};
        try{
    
    
            array=null;
            System.out.println(array[2]);
            System.out.println("haha");
        }catch (ArrayIndexOutOfBoundsException e){
    
    
            e.printStackTrace();//通过e打印栈上的信息
            System.out.println("已捕捉一个数组越界异常");
        }
        System.out.println("hello");
    }
}

我们在原先代码上做一些改动,把原先数组下标越界改为允许范围,但是在前面将array这个引用指向null,这样会发生什么呢?
在这里插入图片描述
这里运行会发生空指针异常,但是因为我们先前设定的catch是捕捉数组越界异常的,所以这里会无法捕捉,这里就会交给JVM,程序会立即终止。

为了避免这种情况,我们一般会多些几个catch
代码示例3.使用多个catch处理对应种类的异常

public class Test {
    
    
    public static void main(String[] args) {
    
    
        int []array={
    
    1,2,3};
        try{
    
    
            array=null;
            System.out.println(array[2]);
            System.out.println("haha");
        }catch (ArrayIndexOutOfBoundsException e){
    
    
            e.printStackTrace();//通过e打印栈上的信息
            System.out.println("已捕捉一个数组越界异常");
        }catch (NullPointerException e){
    
    
            System.out.println("已捕捉一个空指针异常");
        }
        System.out.println("hello");
    }
}

在这里插入图片描述
在try中发生异常之后,需要在对应的catch里面,捕捉对应的异常,如果你没能成功捕捉异常,则会交给JVM处理,JVM会立即终止程序。

学到这里可能会有同学问,既然我们所有异常都是Exception这个异常类的子类,那我们可不可以直接catch父类Exception来达到一劳永逸的效果?这样也不用写那么多catch了

答案是——可以用catch父类的方式来捕捉异常,但其捕捉范围太大了,你用这种方式必须打印栈上的信息,否则无法知道到底是哪里错了.

代码示例5.catch一个父类来捕捉所有异常
示例1://不打印栈上信息


public static void main(String[] args) {
    
    
        int[] arr = {
    
    1, 2, 3};
        try {
    
    
            arr = null;
            System.out.println(arr[2]);
            System.out.println("haha");
        } catch (Exception e) {
    
    
            System.out.println("捕捉到一个异常");
        }
        System.out.println("hello");
    }

在这里插入图片描述
示例2://打印栈上信息

public static void main(String[] args) {
    
    
        int[] arr = {
    
    1, 2, 3};
        try {
    
    
            arr = null;
            System.out.println(arr[2]);
            System.out.println("haha");
        } catch (Exception e) {
    
    
            e.printStackTrace();
            System.out.println("捕捉到一个异常");
        }
        System.out.println("hello");
    }

在这里插入图片描述
还有一个需要注意的是:catch里面父类不能排在子类前面

public static void main(String[] args) {
    
    
        int[] arr = {
    
    1, 2, 3};
        try {
    
    
            arr = null;
            System.out.println(arr[2]);
            System.out.println("haha");
        } catch (Exception e) {
    
    
            e.printStackTrace();
            System.out.println("捕捉到一个异常");
        }catch (ArrayIndexOutOfBoundsException f){
    
    
            f.printStackTrace();
            System.out.println("捕捉到一个异常");
        }
        System.out.println("hello");
    }

在这里插入图片描述
为什么这里会报错呢?父类如果放前面,catch就把父类全部找了一遍了,子类里的异常在父类就被找了,那子类存在的意义是什么呢?

所以,我们约定:catch在捕获异常时,从上往下,最好是子类->父类,当然,最好是捕捉具体的异常,尽量避免捕捉父类,减少错误发生可能。

代码示例6.finally 表示最后的善后工作, 例如释放资源

public static void main(String[] args) {
    
    
        Scanner scanner=new Scanner(System.in);
        try{
    
    
            int n=scanner.nextInt();
            System.out.println(10/n);
        }catch (InputMismatchException e){
    
    
            e.printStackTrace();
            System.out.println("输入错误");
        }catch (ArithmeticException e){
    
    
            e.printStackTrace();
            System.out.println("算术异常,可能0作为了除数");
        }finally {
    
    
            //finally一般用作资源的关闭
            scanner.close();
            //Scanner也是一种资源,类似文件,打开后,不需要了,最好关闭
        }
        System.out.println("after");
    }

可能有同学对上面代码做了测试,比如:
正确输入所需数据,catch后代码会执行
在这里插入图片描述
错误输入数据,catch后代码也会执行
在这里插入图片描述
那既然有错没错,我们catch后代码都会执行,那final存在的意义是什么呢?
我们要注意:错误输入数据catch后代码执行的前提是我们捕捉到了那个异常

假如我们没有捕捉到那个异常呢?比如我们删掉上面的一部分代码

public static void main(String[] args) {
    
    
        Scanner scanner=new Scanner(System.in);
        try{
    
    
            int n=scanner.nextInt();
            System.out.println(10/n);
        }catch (ArithmeticException e){
    
    
            e.printStackTrace();
            System.out.println("算术异常,可能0作为了除数");
        }finally {
    
    
            //finally一般用作资源的关闭
            System.out.println("finally已执行");
            scanner.close();//Scanner也是一种资源,类似文件,打开后,不需要了,最好关闭
        }
        System.out.println("after");
    }

我们把输入非数字的异常捕获删掉了,那我们这个时候来试试捕获失败会发生什么
在这里插入图片描述
catch后的代码不再执行,但是final依然执行。

综上——无论是否存在异常, finally 中的代码一定都会执行到. 保证最终一定会执行到 Scanner 的 close 方法。

ps:还有一种可以后续关闭资源的写法,大家可以参考一下

public static void main(String[] args) {
    
    
        try(Scanner scanner=new Scanner(System.in)){
    
    
            int n=scanner.nextInt();
            System.out.println(10/n);
        }catch (ArithmeticException e){
    
    
            e.printStackTrace();
            System.out.println("算术异常,可能0作为了除数");
        }finally {
    
    
            //finally一般用作资源的关闭
            System.out.println("finally已执行");
        }
    }

try后面加一个小括号,把资源放到小括号内,finally里面就不需要有close了,try会自动帮助你进行关闭资源(虽然try帮助你关闭资源了,finally依然会执行)。

2.2异常处理流程

1.程序先执行 try 中的代码

2.如果 try 中的代码出现异常, 就会结束 try 中的代码, 看和 catch 中的异常类型是否匹配.

3.如果找到匹配的异常类型, 就会执行 catch 中的代码

4.如果没有找到匹配的异常类型, 就会将异常向上传递到上层调用者.

5.无论是否找到匹配的异常类型, finally 中的代码都会被执行到(在该方法结束之前执行).

6.如果上层调用者也没有处理的了异常, 就继续向上传递.
一直到 main 方法也没有合适的代码处理异常, 就会交给 JVM 来进行处理, 此时程序就会异常终止.

2.3抛出异常

除了 Java 内置的类会抛出一些异常之外, 我们也可以手动抛出某个异常. 使用 throw 关键字完成这个操作.

public static void func(int x) throws RuntimeException {
    
    
//最好用throws声明一下要抛出异常的种类
        if(x==0){
    
    
           throw new RuntimeException("x=="+x);//自定义一个异常
        }
    }
public static void main(String[] args) {
    
    
        func(0);
    } 

在这里插入图片描述

我们可以根据实际情况来抛出需要的异常. 在构造异常对象同时可以指定一些描述性信息

2.4异常说明

我们在处理异常的时候, 通常希望知道这段代码中究竟会出现哪些可能的异常。我们可以使用 throws 关键字, 把可能抛出的异常显式的标注在方法定义的位置. 从而提醒调用者要注意捕获这些异常。

public static int divide(int x, int y) throws ArithmeticException {
    
     
 if (y == 0) {
    
     
 throw new ArithmeticException("抛出除 0 异常"); 
 } 
 return x / y; 
} 

2.5关于finally的注意事项

我们前面知道,finally里面的代码一定会被执行,这时候可能会产生一些冲突

public static int func() {
    
    
        try {
    
    
            return 10;
        } finally {
    
    
            return 20;
        }
    }
public static void main(String[] args) {
    
    
        System.out.println(func());
    }   

我们func里面的try已经返回一个数字10了,但是由于finally一定会被执行,所以finally里面也返回了一个数字20,那我们到底是返回了啥呢?
在这里插入图片描述
finally 执行的时机是在方法返回之前(try 或者 catch 中如果有 return 会在这个 return 之前执行 finally). 但是如果finally 中也存在 return 语句, 那么就会执行 finally 中的 return, 从而不会执行到 try 中原有的 return.
一般我们不建议在 finally 中写 return (会被编译器警告)

三、java异常体系

在这里插入图片描述
所有的异常都来自Throwable,Throwable下面又分两个叫作Error(错误)和Exception(异常),Exception下面分有两大类:1.运行时异常(又称非受查异常),2.编译时异常(受查异常)。
ps:Exception是我们程序员所使用的所有异常类的父类
其中 Exception 有一个子类称为 RuntimeException , 这里面又派生出很多我们常见的异常类NullPointerException , IndexOutOfBoundsException 等

举例说明
1.错误
如果出现错误,我们程序员必须亲自处理代码,那什么是Error呢?我们来看一段递归代码:

public static void func(){
    
    
        func();
    }

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

在这里插入图片描述

我们调用了一个递归,但是忘记给它加判断条件了,它一直在栈上开辟空间,把栈挤爆了,这就是栈溢出错误。当然也有可能会是堆被挤爆的情况(但堆一般都很大,被挤爆几率比较小)。

2.运行时异常(非受查异常)
比如空指针异常、算术异常、数组下标越界异常等等,这里仅举例空指针

public class Test {
    
    
    public int num = 10;
    public static void main(String[] args) {
    
    
        Test t = null;
        System.out.println(t.num);
   }
}

3.编译时异常(受查异常)

public static String readFile() {
    
    
        // 尝试打开文件, 并读其中的一行.
        File file = new File("d:/test.txt");
        // 使用文件对象构造 Scanner 对象.
        Scanner sc = new Scanner(file);
        return sc.nextLine();
    }
    public static void main(String[] args) {
    
    
        System.out.println(readFile());
    }

在这里插入图片描述

比如上面这个图片里所示代码,系统自动给你划了红线,你鼠标放上去显示FileNotFoundException这种的异常就是受查异常,不显示处理就无法编译通过。

受查异常的显示处理方式有2种:
1.用try catch包裹:

public static void main(String[] args) {
    
    
        System.out.println(readFile());
    }
    public static String readFile() {
    
    
        File file = new File("d:/test.txt");
        Scanner sc = null;
        try {
    
    
            sc = new Scanner(file);
        } catch (FileNotFoundException e) {
    
    
            e.printStackTrace();
        }
        return sc.nextLine();
    }

2.在方法上加上异常说明, 相当于将处理动作交给上级调用者

public static void main(String[] args) {
    
    
        try {
    
    
            System.out.println(readFile());
        } catch (FileNotFoundException e) {
    
    
            e.printStackTrace();
        }
    }
    public static String readFile() throws FileNotFoundException {
    
    
        File file = new File("d:/test.txt");
        Scanner sc = new Scanner(file);
        return sc.nextLine();
    }

四、自定义异常类

Java 中虽然已经内置了丰富的异常类, 但是我们实际场景中可能还有一些情况需要我们对异常类进行扩展, 创建符合我们实际情况的异常。

比如我们现在写一个用户登录功能

public class TestDemo {
    
    
    private static String name="bit";
    private static String password="123";

    public static void login(String name,String password) {
    
    
       if(!TestDemo.name.equals(name)){
    
    
           System.out.println("用户名输入错误");
       }
       if(!TestDemo.password.equals(password)){
    
    
           System.out.println("密码输入错误");
       }
    }
}

如果我们想在用户输入用户名或者密码错误的时候抛出异常,我们可以基于需要创建我们自己的异常类

4.1自定义受查异常示例

class UserError extends Exception {
    
    
 //自己自定义的一个异常必须继承一个异常,
    public UserError(String message) {
    
    
        super(message);
    }
}
class PasswordError extends Exception {
    
    
    public PasswordError(String message) {
    
    
        super(message);
    }
}

然后我们将原来的login方法稍做改动

public static void main(String[] args) {
    
    
        try {
    
    
            login("admin", "123456");
        } catch (UserError userError) {
    
    
            userError.printStackTrace();
        } catch (PasswordError passwordError) {
    
    
            passwordError.printStackTrace();
        }
    }
    public static void login(String userName, String password) throws UserError,
            PasswordError {
    
    
        if (!Test.userName.equals(userName)) {
    
    
            throw new UserError("用户名错误");
        }
        if (!Test.password.equals(password)) {
    
    
            throw new PasswordError("密码错误");
        }
        System.out.println("登陆成功");
    }

4.2自定义非受查异常示例

class NameException extends RuntimeException{
    
    
    //自己自定义的一个异常必须继承一个异常,
    //比如我们这里自定义的姓名异常继承了RuntimeException
    public NameException(String message){
    
    
        super(message);
    }
}
class PasswordException extends RuntimeException{
    
    
    public PasswordException(String message){
    
    
        super(message);
    }
}
public class TestDemo {
    
    
    private static String name="bit";
    private static String password="123";

    public static void login(String name,String password) {
    
    
       if(!TestDemo.name.equals(name)){
    
    
           throw new NameException("用户名输入错误");
       }
       if(!TestDemo.password.equals(password)){
    
    
           throw new NameException("密码输入错误");
       }
    }

    public static void main(String[] args) {
    
    
        login("bit","1234");//我们这里把密码写错,测试一下
    }
}

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/m0_57180439/article/details/121442722
今日推荐