夯实基础,美团大佬带你深入理解 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 的异常默认是非受查异常.
今日份分享已结束,请大家多多包涵和指点!
评论