写点什么

夯实基础,美团大佬带你深入理解 Java 中的异常体系,再也不踩坑了

发布于: 2021 年 06 月 10 日
夯实基础,美团大佬带你深入理解Java中的异常体系,再也不踩坑了

今日分享开始啦,请大家多多指教~

一、初识异常

经常有同学看到异常来问了,异常到底是什么? 而在我们之前的学习中,我们其实已经接触到了 Java 当中的异常。

1.算数异常

首先我们遇到的第一个异常是我们在讲除号运算符时遇到的——算数异常。

见以下代码

我们在分子的位置出现了 0,来运行以上代码。

运行结果:

2.数组下标越界异常

在数组篇我们也提到了数组越界的问题

我们来看以下代码:

运行时结果:

查看异常的信息

3.空指针异常

空指针异常也在之前的学习中经常出现

看以下代码

我们将 array 数组置为 null ,之后再去访问这个数组,就出现了空指针异常。

运行结果:

这些异常都是需要我们在平时所积累出来的,所谓异常指的就是程序在运行时出现错误时通知调用者的一种机制。

关键字 “运行时”

有些错误是这样的,例如将 System.out.println 拼写错了,写成了 system.out.println. 此时编译过程中就会出错,这是 “编译期” 出错。

而运行时指的是程序已经编译通过得到 class 文件了,再由 JVM 执行过程中出现的错误。

4.异常的处理方式

异常的种类有很多,不同种类的异常具有不同的含义,也有不同的处理方式。(在此了解即可)

防御式编程

错误在代码中是客观存在的,因此我们要让程序出现问题的时候及时通知程序猿,我们有两种主要的方式。

LBYL: Look Before You Leap.

在操作之前就做充分的检查。

EAFP: It’s Easier to Ask Forgiveness than Permission.

“事后获取原谅比事前获取许可更容易”,也就是先操作,遇到问题再处理。

其实很好理解,打一个非常形象的比喻来理解啊:

比如说有一个你非常喜欢的女生,你想要去拉她的手,那么有几种方式呢?

第一种方式 LBYL:问一下:我能拉你的手吗?

这就是操作之前仔细检查。

第二种方式 EAFP:直接先把手拉上,她要是甩开了再说其他的,要是没拒绝就拉着呗。

这就是”事后获取原谅比事前获取许可更容易“,也就是先操作,遇到问题再处理。了解即可,不用特别去记忆。

5.异常的好处

我们看一下,上述的两种风格在处理代码时究竟是怎样的呢?

我们先给一个特定的场景啊,处理王者荣耀游戏开局时的异常代码

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

代码的每一步执行完都要进行检查,确认正确才能进行下一步。这就是在操作之前做检查。

EAFP 风格的代码

在这里我们能体会到 Java 风格的代码异常处理

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

二、异常的基本语法

接下来,我们就正式开始了 Java 当中处理异常的基本语法讲解了。

1.基本格式

Java 当中处理异常的基本格式

2. 是否处理异常对程序的影响

我们来看一组代码:

这组代码中,在代码执行的第二步中,我们存在着数组越界异常,那么第三步的 “hello” 是否会打印呢?

我们运行程序,看结果

“hello” 并没有打印,那么这是为什么呢?此时程序出现异常了,而当程序出现异常时,那么代码将不会被执行。

那么我们还是想执行这个“hello”,那么我们该怎么办呢?

我们就将代码写成以下格式的代码:

运行结果:

hello 也成功进行了打印。也就是说,在这种方式下,代码抛出异常,捕获异常之后,代码将继续向后执行。

那么又有同学说了,我们定义的那个 e 没有用到啊,我们再来看 e 的用处

e.printStackTrace — 打印出现异常栈的追踪

我们来看这时程序运行结果

这时就把出现异常的栈的位置打印出来了,这就是 e 的一个作用吧。

那么又有同学来问了,为什么之前的 “hello”无法打印出来,之后的 try…catch 捕捉异常之后能够打印出来呢?

3.用 try … catch 需要注意的问题

1.在 catch 块中,一定要捕获相对应的异常,如果程序抛出的异常在 catch 块当中,不能被捕获,那么就会交给 JVM 处理。

看以下代码:

在 catch 块当中并没有捕获到 数组越界异常,我们来看一下运行结果。

直接交给 JVM 处理,程序终止,不再向下执行。

2.可以通过 catch 捕获多个异常

我们可以在 try 之后跟上多个 catch 来捕获异常,如以下代码:

运行程序结果如下:

我们知道,所有的异常都继承于 Exception,那么有人问了,我们可以直接捕获一个 Exception 的异常吗?

我们来试一下,我们知道捕捉异常的顺序是按照代码书写的顺序执行的,大家看一下这段代码

当 Exception 这个父类异常放在开头,那么下面的捕捉异常则进行报错。

如果这样写,那么 Exception 后面所有的异常都失效了,为什么呢,因为不管 try {} 里面是什么异常,都会在 Exception 这一步进行捕获到,后面的捕获异常自然失效了。

所以当我们在前面写了 Exception 这个捕获异常时,后面就不要再进行捕获其他异常了。

那么又有同学说了,我们为了省事,那么我们以后都捕获 Exception 这个 异常不就好了?

当然不行,在这种情况下,我们不能区分我们捕获到的是什么异常。所以,不建议大家直接捕获到一个 Exception 的异常。

3.不建议大家直接捕获一个 Exception 的异常

有些同学想要省事,想要将两个异常合并成一条进行捕捉,我们只需要将两个异常用 | 进行连接即可。

给个示例:

看一下运行结果:

成功运行。

总结

1.在 catch 块中,一定要捕获相对应的异常,如果程序抛出的异常在 catch 块当中,不能被捕获,那么就会交给 JVM 处理。

2.可以通过 catch 捕获多个异常

3.不建议大家直接捕获一个 Exception 的异常

4.可以用 | 同时处理两个异常,如上例。

4.关于异常的处理方式

异常的种类有很多,我们要根据不同的业务场景来决定。

对于比较严重的问题(例如和算钱相关的场景),应该让程序直接崩溃,防止造成更严重的后果。

对于不太严重的问题(大多数场景),可以记录错误日志,并通过监控报警程序及时通知程序猿。

对于可能会恢复的问题(和网络相关的场景),可以尝试进行重试。

在我们当前的代码中采取的是经过简化的第二种方式,我们记录的错误日志是出现异常的方法调用信息,能很快速地让我们找到出现异常的位置,以后在实际工作中我们会采取更完备的方式来记录异常信息。

关于 “调用栈”

方法之间是存在相互调用关系的,这种调用关系我们可以用 “调用栈” 来描述。在 JVM 中有一块内存空间称为 “虚拟机栈” 专门存储方法之间的调用关系,当代码中出现异常的时候,我们就可以使用 e.printStackTrace(); 的方式查看出现异常代码的调用栈。

5.finally 的使用

我们再来看一下异常基础语法的学习

我们看到了 try…catch 之后还可以跟着 finally

那我们来说一下 finally 的特性

不管 这个代码 是否抛出异常,finally 的 内容都会被执行。所以 finally 经常来做一些善后的内容。比如:关闭资源我们来看一下这一组代码

考一下大家,这组代码执行的结果是什么呢?

运行结果如下:

打印结果为 2,这是为什么呢?

首先我们需要明确的一点是,finally 的内容一定会被执行。

我们分析一下:

在 try 块当中,在打印 array[4]时出现异常,后面的 return 语句就不再执行了,所以最后执行 finally 块,返回 2.

我们再来看一个代码示例:

这组代码执行的结果是什么呢?

运行结果:

最后返回了 2。

这又再次明确了一点:

finally 的内容是一定会被执行的

finally 的使用 总结:

1.finally 块当中的代码终究会被执行的

2 .不建议在 finally 当中出现 return 语句.

6.异常处理流程

好了,到现在,我们算是讲清楚了 try…catch…finally 及异常处理的流程等问题,那么大家以后在写代码的过程中,一定要记得去使用 try…catch,不能一味地交给 JVM 来处理它,好了我们开始下一块内容——抛出异常。

7.抛出异常

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

throw 一般抛出一个你想要抛出的异常(或者自定义的异常)

(1)throw 的使用

我们来看代码示例:

我们用 throw new 了一个算数异常,为什么要 new 呢? 因为算数异常本身也是一个类,也要实例化。

我们来看运行时结果:

成功地抛出异常了,但是这样写有一个不好的地方。我们抛出了一个异常但是呢,这个异常我们只是抛出了但是并未处理,所以最后程序出现异常后交给 JVM 处理,程序最后终止。

(2)声明异常

对于我们调用 devide 方法的人来说啊,如果 devide 方法的内容很多,我们就看不出 devide 会抛出一个异常。那么为了让调用 devide 方法的人知道,我们调用这个方法会抛出这个异常,一般情况下,我们会给这个方法进行声明异常。那么怎么声明呢?

通过 throws 声明这个方法会抛出一个异常下面我们来看代码示例:

方法调用者知道了调用该方法可能会抛出 算数异常,就用上了 try…catch 来捕获异常。

运行结果:

(3)小结

三、Java 异常体系

那么异常到底有多少种?要了解这个问题,我们就需要知道 Java 的异常体系了。

上图并没有将所有的异常都列举出来,只是大概演示一下。

在 Java 当中,我们所看到的异常,其实也就是对应着一个类。

由上图中我们可以看到,整个 Java 异常体系都是继承于顶层类 Throwable,那么 Throwable 就是所有异常、错误的父类。

对于 Throwable 来说,直接继承这个类的有两个子类,

Error ( 错误 ) 和 Exception(异常)。

我们来看一下 jdk_api 帮助手册中对 Throwable 的解释

我们这篇讲的是异常,怎么又出现一个错误 Error 呢?

我们也来认识以下 Error,比如说我们写一个代码:

运行之后出现以下结果:

我们来对比一下:

异常是以 Exception 结尾的,而错误是以 Error 结尾的。

对于 Error 来说——这种错误一定得由程序员自己解决。

而对于 Exception 来说——异常时可以由程序自己解决的。

而异常又有以下划分

运行时异常 Runtime Exception 就是我们上面提到的算数异常、数组越界异常、类型转换异常等等,那么就有同学问了?

什么是运行时异常?

运行时异常就是在程序运行的时候抛出的异常。

什么是编译时异常?

编译时异常就是在程序编译时抛出的异常。如果一段代码可能抛出受查异常,那么必须显式进行处理。

显式处理的方式有两种:

a) 使用 try catch 包裹起来;

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

别忘了 IDEA 神奇的 alt + enter,能够快速修正代码。

小结

四、自定义异常类

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

我们来实现一个简单的自定义的异常类

首先我们要自定义一个异常类,同时继承一个父类异常。

那么这个我们自定义的异常怎么用呢?

下面我们来看

运行结果:

这就是我们自定义异常的使用。

我们再来一个代码示例,下面我们给一个真实的业务场景。

例如,我们实现一个用户登陆功能。

此时我们在处理用户名密码错误的时候可能就需要抛出两种异常,我们可以基于已有的异常类进行扩展(继承),创建和我们业务相关的异常类。

此时我们的 login 代码可以改成

自定义类的注意事项:

1.自定义异常通常会继承自 Exception 或者 RuntimeException

2.继承自 Exception 的异常默认是受查异常

3.继承自 RuntimeException 的异常默认是非受查异常.

今日份分享已结束,请大家多多包涵和指点!

用户头像

还未添加个人签名 2021.04.20 加入

Java工具与相关资料获取等WX: pfx950924(备注来源)

评论

发布
暂无评论
夯实基础,美团大佬带你深入理解Java中的异常体系,再也不踩坑了