java异常深入理解与提升(含面试题)

前言

作为一个java开发者,异常肯定是天天都在使用的,但是很多时候我们对异常的了解、认知都只是停留在表面。好像现在不会多线程、高并发、微服务、性能调优、容器……这些高逼格的东西,都不好意思说自己是java开发,诚然,这些东西是很重要,但是基础、本质的东西一样重要。今天我就“详细、深入”的学习一下java中的异常,包括体系结构、异常处理、自己使用、异常本质、细节等等,吃透这些东西,能让我们更好的理解、学习java,当然,异常相关的面试题也将不在话下。当然,受限于个人水平、认知、经验等等因素,如果发现错误,还请指正。

一、 异常体系结构

  1. 说到异常就不得不说错误(Error), Exception和Error都直接继承于Throwable(可抛出的意思,它的直接父类是Object),Error一旦发生,就只有一个结果,那就是程序终止,jvm退出,Error是不可处理的。
  2. Exception下面有很多内容,但大概分成两部分,RuntimeException和Exception的直接子类。NullPointerException、ClassCastException都属于RuntimeException的直接子类。
  3. 所有RuntimeException及其子类,都被称为运行时异常,运行时异常,在编写程序的时候,可以选择处理,也可以不处理。我听到过一个说法:运行时异常都是自己在写代码的时候可以避免的,所以没有必须要程序员在代码中进行处理。而编译时异常,大多数时候是在使用外部资源的时候有的(比如文件读取、格式化等等),因是外部资源,无法保证一定稳定,所以强制要求程序员提前进行预防。不知道对不对,但是我感觉挺有道理的。
  4. 所有Exception的直接子类,都被称为编译时异常。编译时异常并不是在编译阶段发生的,是表示在编写程序的时候必须预先对这种异常进行处理,不处理编译器会报错。
    异常体系
    (这张图我也忘了在哪里保存的呢,所以这里就不贴摘抄自哪里了)

二、 异常处理

异常是可以处理的,所以在需要时,我们可以对其进行处理。只不过在大多数时候,我们处理的是编译时异常,因为编译时异常必须要我们进行处理。方式主要有两种,抛给上级和内部消化。

1. 抛给上级处理

使用throws将异常抛给上级处理,意思就是这个异常我自己处理不了,需要抛给上级(也就是该方法的调用者)处理,如果采用这种方式处理异常,那么发生异常后面的代码是不会被执行的。上级也有两种处理方式,继续往上抛,或者try……catch。注意,如果是在main方法中,最好不要忘上抛,因为main方法的调用者是JVM,JVM对于异常的处理方式就是:终止程序,打印异常堆栈追踪信息到控制台,这样的异常处理是没有任何意义的。

2. 内部消化

自己内部消化就是:使用try……catch代码块,对于任何异常,包括编译时异常和运行时异常,都可以用Try……catch来处理,如果发生的异常被捕捉到了,那么tyr……catch代码块后面的代码还是会继续执行的。

3. 注意事项

无论是抛给上级还是使用try……catch,都只是一种提前预防的措施,并不意味着一定会发生异常。使用throws抛给上级的意思是:如果发生了异常,自己不处理,抛给上级,如果没发生,则正常执行。try……catch也是如此,发生并且捕捉到了这个异常,就执行catch里的代码,不发生则try里的代码正常执行。

三、自己使用

自己使用异常的方式大抵有两种,

方式一

直接new RuntimeException或者Exception,构造方法中传入错误信息,然后抛出该对象。同样的,这个对象被抛出之后,后面的代码也就不会执行了。不同的是,如果我们只是new并且抛出了RuntimeException,无需额外处理,而如果是new并且抛出了Exception,那么必须得处理,说起来有点搞笑,我自己抛出的,又要我自己处理。但通常情况下,我们在代码内部抛出Exception,处理方式不是自己又使用try……catch将异常捕获,而是在方法签名上使用throws抛给方法的调用者,这样做的目的是提醒方法调用者,这里可能存在异常,你调用我这个方法,就要提前预防。

  • 抛出RuntimeException,无需额外的处理
    抛出RuntimeException
  • 抛出Exception
    抛出Exception抛出Exception,需要在方法前面上用throws声明,其实就是提醒调用者,这个方法很大可能会出现异常,强制调用者提前预防。
方式二

先创建一个类,继承自RuntimeException或Exception,然后在使用的时候,抛出该类的对象。抛出这个异常对象,后面的代码也不会被执行了。同样存在与方式一相同的情况,继承RuntimeException时无需额外处理,而继承Exception,需要在方法签名上进行声明。

  • 自定义异常,比较简陋
    自定义异常
  • 使用,抛出是无需额外处理
    使用
    继承Exception于上面类似,不同的是:和方式一异常,在抛出异常的方法签名上,需要声明。

四、一些细节

1. 异常的本质

异常的本质是什么呢?其实就是在发生异常是,new一个相应的异常对象,然后将其抛出的过程。我们在使用try……catch的时候,catch里不是可以捕捉到一个对象吗,这个对象就是发生错误的时候,创建出来并且被抛出的对象。代码测试:异常本质
运行一下主方法,会发现打印的是true,==在比较对象时,比较的地址值,所以得出结论:catch到的这个e实际上就是 抛出时new的那个对象。

2. 编译时异常是编译时发生的异常吗?运行时异常就是运行时发生的异常吗?为什么?

编译时异常并不是在编译时发生的异常,编译时如果发生了错误,编译时不能通过的,编译只是对语法和简单的逻辑做了判断。例如:少写了一个分号这就是语法,在return后面写了代码,不能被执行,这是简单的逻辑判断。

无论是编译时异常还是运行时异常,都是发生在运行阶段的,因为只有在运行阶段,才能new对象,并且将其抛出。只是编译是异常,在写代码的时候,必须对这个编译时异常进行处理。

3. 编译时异常和运行时异常的区别以及选择

可能大家都知道了,编译时异常需要在写代码的时候,就需要对这个异常进行处理,而运行时异常,则不需要。但是为什么要这么设计呢?我们自己在使用使用异常的时候,又该如何选择呢? 其实,编译时异常,是一个相对概率较大的事件,就像天气预报说今天下午要下雨一样,很大可能会下雨,那我们会提前带伞,做好准备。编译时异常也是如此,因为发生的可能性相对较大,所以,java才设置了这样一个机制,要求我们在写代码的时候,就提前预防这个异常。那为什么不会要求我们在写代码时对运行时异常提前预防呢?因为运行时异常发生概率相对较小,并且如果代码合理,是可以避免的。控制得当,运行时异常的发生就像天上掉下来陨石砸到你一样,是非常小概率的事件。如果我们成天都戴着一个钢盔,预防天上掉陨石,生活中还可能存在其它各种各样的危险呢,杞人忧天,生活岂不是很累。代码也是如此,如果我们大量的代码,都用来提前预防运行时异常,那我们的代码将会非常的臃肿。 而且,运行时异常的发生,大都是因为我自己的逻辑出了问题。

4. throw和throws的区别

throw是用在方法体中,抛出一个具体的异常对象。而throws是用类方签名上,抛出的是异常类型,这个类型表明了,我们在上级对这个异常进行处理的时候,是针对这个异常的类型而言的:比如某个方法存在一个IOException,那么我们在抛出或者try……catch的时候,就只能是针对这个IOException或者它的父类而言。其实这也多态的一种体现,抛出的某个类型的对象,我们用它的父类来接收。throws可以抛出多种类型,而throw只能抛出一个异常对象。

5. 重载和重写中对异常的要求

重载和重写本事两个完全不同的概念,但是在中文里,名字很相似,所以经常被拿来进行比较,那他们各自对异常的要求有如何呢?

(1) 重载

重载存在的意义是,同一个类中存在作用相同的方法,但是他们接受的参数"不同",为了我们不必打破脑袋去为这些方法取名字,所以出现了重载。比如我们常用的println(),
println

他可以接受基本上任何类型的参数,但是作用基本类似,输出在控制台上。如果没有重载,那么我们今天用的可能就是printlnInt()、printlnString()、printlnObject()……了
所以重载的核心要义是:方法参数不同,具体的不同为:参数的个数、类型、循序不同。完整的定义为:同一个类中,方法名相同,方法参数不同(个数、类型、循序)。有必要解释一下,顺序指的是add(int a, double b) 和add(double b 和 int a)这样的顺序不同,可以构成重载,而不是指add(int a, int b)和 add(int b, int a)顺序不同。
除了上面叙述的,其他的因素都不是构成重载的条件,例如:仅仅是返回值类型不同、访问修饰符不同、抛出异常不同。

(2) 重写

重写是值两个具有继承关系的类(这里的继承关系泛指继承和实现),子类对父类(这里子类父类中的类“类”也泛指接口、抽象类)中的方法进行覆盖重写,都说继承是多态的前提,重写一样是多态的前提。有了重写,子类才能对父类中的方法进行覆盖,父类类型的引用指向不同类型的子类对象时,才能展现出不同的面,否则也是千篇一律和父类一样,没有多态只说。所以重写的要求很多,可以概况为一句顺口溜:两同两小一大,具体含义为

  • 两同:方法名相同,参数列表相同(个数、类型、顺序)
  • 两小:子类方法的返回值类型和抛出的异常必须比父类的小或相同
  • 一大:子类方法的访问权限修饰符比父类方法更高或者相同

其他的我们不聊更详细了,主要详细说一说,有几点细节需要注意

  • 注意这里的异常是只编译时异常,运行时异常不再此列,随便抛。就算父类方法没抛异常,子类重写都可以抛运行时异常。
    还记得多线程中的Thread类吗?它的run方法是要执行的内容,我们可以继承Thread类,然后重新run方法,不知道有没有主要到这个细节:在run方法里面如果调用了抛出了编译时异常的方法,摁快捷键alt + enter弹出的处理方法中,只有try……catch。
    在这里插入图片描述

而如果是在其他方法中
在其他方法中

会多出一个Add exception to method signature的选项,这句话的意思是:将添加异常到方法签名上,简单点就是在方法头上用throws抛出。在重写run方法的时候没有这个选项,那我硬要加上呢?
在这里插入图片描述

那么编译器编译不会通过,直接飘红。
但是如果抛出的是运行时异常,那么则可以正常编译并且运行
在这里插入图片描述但是抛出允许时异常,是没有解决调用方法抛出的编译时异常的。

  • 子类方法抛出的异常个数没有限制,只要满足类型是父类方法抛出异常的子类或相同即可。
6. try……catch的使用细节
  1. 一个try可以搭配多个catch,自上而下的顺序,注意父类型异常必须写在子类型异常的下面,不然会捕捉不到子类型异常。另外,即便搭配了多个catch,意思是可以捕获多种类型异常中的一个,不能捕捉多个异常,因为一旦try中发生异常,那么try中发生异常后面的代码就不会继续执行加了。
    在这里插入图片描述

  2. jdk8的新特性:一个catch里可以捕捉多种类型的异常,在catch中用“|”分开
    在这里插入图片描述

  3. try……catch一起不用finally可以,try……finally一起不用catch也可以,感兴趣的自己去尝试效果

7. finally和return的权力之争

finally的作用是:里面的代码块无论如何都会执行
return的作用是:结束当前方法,并且返回返回值(如果有)
就好像世界上无坚不摧的矛碰到了世上牢不可破的盾一样,那他们俩相碰的结果又会是怎样呢?
而且还有许多这样的面试题,我们先来看一看这些面试题。

1. 面试题

这些不是真正的面试题,是我凭借我做过的这方面的面试题的记忆和这块知识的关键点编的,和真正的面试题也差不多。我觉得我编的这些更具有联系性,这些题由浅入深,设计巧妙……哈哈哈。我没有给出答案,自己敲去运行一下记忆更深刻。

  1. 调用以下方法会输出出什么?
    面试题1

  2. 调用以下方法会输出出什么?(注意和题1区分)
    面试题2

  3. 调用以下方法返回多少?输出多少?
    面试题3

  4. 调用以下方法返回多少?
    面试题4

  5. 调用以下方法返回多少?
    面试题5

2. 结论

结果了上面题目的摧残,还是总结一下结论

  1. finally比return的权力更大,同时存在finally和return时,finally还是会执行。问题:那还有比finally权力更大的吗?答案是有的,比如System.exit(0);退出虚拟机,或者断电、关机等物理攻击。
  2. 如果finally中没有return,那么会在执行了finally之后,将前面的return返回;如果finally中对前面返回的那个值进行了修改,返回的值是不会受finally中代码的影响的,因为前面被返回的值已经被存入了一个临时变量中,finally执行完毕,返回的也是那个临时变量。如果前面和finally中都使用return返回了一个变量,那么finally中的那个会覆盖之前的。

就想到这么多,但是其实我没有全部总结,就我知道的:在使用流时,可以将 获取流的代码写在try( )中,然后不用手动释放,会自动释放。

受限于个人水平,如果哪里有错,还请指正。

  • 6
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值