写了挺久的代码,却还被"异常"支配?

共 6232字,需浏览 13分钟

 ·

2021-02-18 21:06

大家好,我是小菜,一个渴望在互联网行业做到蔡不菜的小菜。可柔可刚,点赞则柔,白嫖则刚!死鬼~看完记得给我来个三连哦!

本文主要介绍 Java 中的异常

如有需要,可以参考

如有帮助,不忘 点赞

微信公众号已开启,小菜良记,没关注的同学们记得关注哦!

面试官

请说一下你平时比较常遇到的运行时异常

小菜

好的,我平时比较常遇到的异常有:NullPointException (空指针异常) 、ClassNotCastException (类型转换异常) 、IndexOutOfBoundException (下标越界异常),emmm.... 还有些忘记名字了~

面试官

那你说下异常的分类吧

小菜

异常好像是分为运行时异常和 ... ,不好意思,有点想不起来了

面试官

emm, 还有个编译时异常,那你平时写代码提示有异常是怎么处理的

小菜

额,这个, 一般都会直接抛出异常

面试官内心OS就这水平?

小菜内心OS伤害性不高,侮辱性极强

对于常年奋战在代码一线的我们来说,说几个常见异常就像让你数一下你有多少钱一样,虽然有,但是说出来不多。又很轻蔑的觉得,这什么面试题,就这?还能当面试官。但是往往这么简单的问题,你答的不好,一样能让你 面试等通知,录取砍薪资

我们从吐槽中回过神来想一想,自己写的代码还没点 x 数吗,异常、bug 不就是自己的精神伴侣吗,没这点东西的支撑,自己平时怎么冠冕堂皇的划水呢!

是什么导致我们平时遇到的异常很多,却记不起几个。是因为实在太多了,让自己记不住吗!还是习惯了百度呢~ emmm,估计都有,小菜心虚了,赶紧奋笔,摆脱被异常支配的烦恼。电子设备面前的你,为了更有底气的回答上面那几个问题,不妨跟小菜再来复习下 异常 吧!

走进异常

异常就是有异于常态,和正常情况不一样,有错误出现。在 Java 中,阻止当前方法或作用域的情况,称之为异常。我们先来看下异常的结构:

Throwable 作为顶层父类,派生出了 ErrorException 两个子类。

  • Error:错误。Error 类以及它的子类的示例,代表了 JVM 本身的错误,错误不能被程序员通过代码处理,Error 一般很少出现。
  • Exception:异常。Exception 类以及它的子类,代表程序运行时发送的各种不期望发生的时间。可以被 Java 异常 处理机制使用,是异常处理的核心。

我们本文重点关注 Exception

Java 的基本理念是 "结构不佳的代码不能运行"

异常使用

一个简单处理异常的例子:

if(t == null){
    throw new NullPointException();
}

当我们需要引用对象 t,但是有可能 t 对象尚未被初始化,所以在使用这个对象之前,我们会对引用进行检查。可以创建一个代表错误信息的对象,并且将它从当前环境中 “抛出”,这样就把错误信息传播到了 “更大” 的环境中,这种称为 抛出一个异常。这样的好处便是当前环境下就不必再为这个问题操心了,它将会在别的地方得到处理。

异常参数

异常对象与其他 Java 对象一样,都可以通过 new 关键字在 堆上 创建异常对象,因此,这也伴随着存储空间的分配和构造器的调用。

所有标准的异常类都有两个构造器,一个是 默认构造器, 一个是 接受字符串作为参数的构造器 这样子我们能把相关的异常信息放入异常对象的构造器中:

throw new NullPointException("t 对象为空");

通过这样子抛出异常,排查者也能快速的定位问题

我们还可以简单地把异常处理看成一种不同的返回机制:

尽管返回的异常对象其类型与方法设计的返回类型不同,但是从效果上看,它就像从方法中返回的。

异常捕获

在编写代码处理异常时,对于检查异常,有2种不同的处理方式:使用try…catch…finally语句块处理它;或者在函数签名中使用throws声明交给函数调用者去解决。

try 的译思便是 尝试,那么是尝试做什么呢?我们知道如果在方法内部抛出了异常(或者在方法内调用的其他方法抛出了异常),这个方法将会在抛出异常的过程中结束。我们有时候不想这么轻易结束,这个时候就用到了 尝试 的概念,我们可以在方法内设置一个特殊的块来捕获异常,在这个块中 "尝试" 各种(可能产生异常的)方法调用,所以我们将其称之为 try 块

有了异常处理机制,我们可以把所有可以产生异常的动作都放进 try 块 里面,然后只需一个地方就可以捕获所有异常。

但是,这里特别需要注意的是,不要滥用异常!!! 有些人可能有点小聪明,编写了以下代码:

咋看代码可以你觉得很奇怪,为什么有人会优先使用基于异常的循环,大部分会这样写的都会以为错误判断机制性能会比较高,因为 JVM 对每次数组访问都要检查是否越界。

注:异常应该只用于异常的情况下,它们永远不应该用于正常的控制流,设计良好的 API 不应该强迫它的客户端为了正常的控制流而使用异常

Java 中提供了三种可抛出结构(throwable) :受检异常(checked exception)、运行时异常(run-time exception)和错误(error)。我们在写代码的时候往往会有所纠结,到底该抛出何种结构?

在决定使用受检异常或者使用未受检异常的时候,我们的主要原则应该是 :如果期望调用者能够适当地恢复程序,这种情况下我们就应该使用受检异常。通过抛出受检异常,我们应该在一个 catch 子句中处理该异常,或者将它传播出去,让调用者处理。

运行时异常错误 都属于 非受检可抛出结构。它们都是不需要也不应该被捕获的可抛出结构。当程序抛出可受检结构的时候,就意味着当前情况属于不可恢复的,如果程序没有捕捉到这样的可抛出结构,将会导致当前线程中断。

我们常用 运行时异常 来表明编程错误。我们实现的所有未受检抛出结构都应该是 RuntimeException 的子类。不应该定义 Error 的子类,虽然 Java 规范 中没有明确要求如此,但是 Error 往往是被 JVM 保留下来使用的,以表明资源不足,约束失败,或者其他使程序无法继续执行的条件。

自定义异常

我们不必深陷 Java 已有的异常类型而无法自拔。Java 提供的异常体系只是包含了基本的异常,不可能预见所有值得报告的错误。所以我们可以自己定义异常类来表示程序中可能会遇到的特定问题。

要自己定义异常类,就必须从已有的异常类中集成,最好是选择意思相近的异常类继承,但是这并不是一个简单的选择~

我们上面只是简单继承了 Exception ,构造函数中无法传入我们想要表达的错误报告,实现这种方式也很简单,我们只需要为异常类定义一个接受字符串参数的构造器:

getMessage() 方法有点类似于 toString(),可以获取异常类更加详细的信息。

栈轨迹

我们平时可以通过打 断点 的方式来调试代码,跟着代码一行一行的走下去,这是因为栈帧 的帮组。当有异常抛出的时候我们也想要有更加详细的信息来追溯异常的源头。

e.printStackTrace() 这个异常的方式是我们捕获异常的时候,系统会自动为我们生成,它的输出格式如下:

当异常的栈轨迹过长时,控制台会刷出一列下来的错误信息,不知道为什么,每次看到这种信息总有种心烦的感觉,真糟糕~ 不知道小伙伴有没有一样的感触。

我们既然不想要这种输出格式,又想要追溯异常的源头,小伙子有够贪心的~

这里便推荐使用 e.getStackTrace() 方法来获取信息。这个方法将返回一个由栈轨迹中的元素所构成的数组,其中每个元素都表示栈中的一帧。数组第一个元素表示的是栈顶元素,并且是调用序列中的最后一个方法调用;数组最后一个元素是调用序列中的第一个方法调用。

image-20210202214952126

这个数组中的元素是 StackTraceElement 类型,我们还可以看下这个类中有哪些API,我们也可以单独输出调用栈方法的方法名:

image-20210202215210524
异常链

我们可以在捕获一个异常后抛出另一个异常,并且希望将原始异常的信息保存下来,这个称之为异常链。

JDK 1.4 之前,开发人员必须自己编写代码来保存原始异常的信息。而现在所有 Throwable 的子类在构造器中都可以接受一个 cause 对象来作为参数,如上述那样 throw new Exception(e)。这个 cause 就用来表示原始异常,这样通过把原始异常传递给新的异常,使得即使在当前位置创建并抛出了新的异常,也能通过这个异常链追踪到最初发生的位置。

标准异常

优先使用标准异常专家级程序员  与 小菜 最主要的区别在于,专家追求并且通常也能够实现高度的代码重用。代码重用 并非谈之尔尔,这是一条通用的规则,异常当然也不例外。Java 平台类库中提供了一组基本的未受检异常,它们满足了绝大多数 API 的异常抛出需求。

为什么要重用标准的异常?

  • 使API更易于学习和使用,因为它与程序员已经熟悉的习惯用法一致
  • 对于用到这些API的程序而言,它们的可读性会更好,因为它们不会出现很多程序员不熟悉的异常
异常描述
NullPointerException访问 Null 对象的方法
IllegalStateException不适合方法调用的对象状态
IllegalArgumentException接收非法参数
IndexOutOfBoundsException下标参数值越界
ConcurrentModificationException禁止并发修改的情况下,检测到对象的并发修改
UnSupportedOperationException对象不支持用户请求的方法
IOException文件读写异常

以上便是我们平时比较常见的可重用异常,开发中应当不要直接用 ExceptionRuntimeExceptionThrowable 或者 Error 。对待这些异常要像对待抽象类一样,你无法可靠地测试这些异常,因为它们是一个方法可能抛出的其他异常的超类。

如果方法抛出的异常与它所执行的任务没有明显的联系,这种情形将会使人不知所措。这个弊端在于除了使排查者感到困惑之外,这也 "污染" 了具有实现细节的更高层的API。

为了避免这个问题,我们需要遵守:更高层的实现应该捕获低层的异常,同时抛出可以按照高层抽象进行解释的异常。这种做法称为 异常转移

因此我们不能为了简便,而直接捕获 Exception 这种异常的超类。

甚至不要忽略异常,我们有时候会以为这个方法不会抛出异常,而因为异常属于 受检异常,不得已我们需要捕获这个异常,但是又自作聪明得不在 catch 块中做任何处理操作。

要忽略一个异常非常容易,但是毫无疑问,你已经给自己埋下了一颗不知什么时候会爆炸而不知道何处爆炸的隐患。空的 catch 块会使异常达不到应有的目的

如果我们一定要选择忽略异常,那么明确的做法应该是:在 catch 块中包含一条注释,说明为什么可以这样做,并且将变量名称命名为 ignored

派生异常

图中 Dog 类继承于 Animal 类,重写了 eat() 方法。当时在我们打算抛出异常的时候,却发现编译器提示报错。纳闷的同时,怀疑了一下这编译器是不是坏了?

事实不是这样的,在继承和覆盖的过程中,某个特定方法的"异常说明的接口"不是变大了而是变小了。这相当于,我父类的方法好好的,被你一继承居然出现了异常,而且我还可能不知道,这不是背地里砸我招牌吗!

finally 使用

对于一些代码,我们希望无论 try 块中的异常是否抛出,它们都能够得到执行。为了达到这个效果,我们可以在异常处理程序后面加上 finally 字句。

这个用处的第一想法便是用来做错误重试,我们可以把 try 块 放入一个循环中,然后加一个计数器或者别的装置,使循环在放弃之前能尝试一定的次数。

finally 内部,无论 try 块 中的代码从哪里返回,都会被执行,何以见得呢?

那么问题又来了!既然 finally 中的语句无论如何都会被执行,那我在 finally 中也有 return ,这个时候返回的是什么?我们不妨试一试。

不知道你是否做对了,答案是返回 finally 中的结果,由此可知:

try 中的 return 语句调用的函数先于 finally 中调用的函数执行,也就是说 try 中的return语句先执行,finally 语句后执行,但try中的 return 并不是让函数马上返回结果,而是 return 语句执行后,将把返回结果放置进函数栈中,此时函数并不是马上返回,它要执行 finally 语句后才真正开始返回。但此时会出现两种情况:

  1. 如果finally中也有return,则会直接返回finally中的return结果,并终止程序,函数栈中的return不会被完成
  2. 如果finally中没有return,则在执行完finally中的代码之后,会将函数栈中保存的try return的内容返回并终止程序

那么如果在 try 中抛出了异常,在 catch 中也有 return,结果又该如何?

没错!还是返回 finally 中的结果,答案已经揭晓,那么我们来总结一下:

1、不管有没有出现异常,finally块中代码都会执行 2、当try和catch中有return时,finally仍然会执行 3、finally是在try中return后面的表达式运算后执行的(此时并没有返回运算后的值,而是先把要返回的值保存起来,管finally中的代码怎么样,返回的值都不会改变,仍然是之前保存的值),所以函数返回值是在finally执行前确定的 4、finally中最好不要包含return,否则程序会提前退出,返回值不是try或catch中保存的返回值

异常使用指南

上面我们复习了一遍Java 中的异常,下面是一段来自 《Java 编程思想》 的摘要。

应该在下列情况下使用异常:

  • 在恰当的级别处理问题。(在知道该如何处理的情况下菜捕获异常)
  • 解决问题并且重新调用产生异常的方法
  • 进行少许修补,然后绕过异常发生的地方继续执行
  • 用别的数据进行计算,以代替方法预计会返回的值
  • 把当前运行环境下能做的事情尽量做完,然后把相同的异常抛到更高层
  • 把当前运行环境下能做的事情尽量做完,然后把不同的异常抛到更高层
  • 终止程序
  • 进行简化(如果ID异常模式是问题变得太复杂,那用起来会非常痛苦也很烦人)
  • 让类库和程序更安全(这既是再为调式做短期投资,也是在微程序的剑专项做长期投资)

异常是 Java 程序设计不可分割的一部分,如果不了解如何使用它们,那你只能完成很有限的工作。这篇异常总结写着写着就挺长了,所以你也要读着读着就会了!路漫漫,小菜与你一同求索~

看完不赞,都是坏蛋

今天的你多努力一点,明天的你就能少说一句求人的话!

我是小菜,一个和你一起学习的男人。 💋

微信公众号已开启,小菜良记,没关注的同学们记得关注哦!

每篇都是初恋的味道~

- END -


浏览 47
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报