JAVA艺术之异常手段
1.什么是异常
异常:程序在运行过程中发生由于外部问题(如硬件错误、输入错误)等导致的程序异常事件。
(在Java等面向对象的编程语言中)异常本身是一个对象,产生异常就是产生了一个异常对象。
2.可能出现错误的错误与问题
1.用户输入错误
用户的输入数据不按要求,例如:你使用Scanner对象,实现键盘输入整数数据到程序中,然后你对这个整数进行一系列处理。这个时候用户可能输入一个字符串,那程序里面没有对字符串数据的处理情况,这个时候就可理解为用户输入错误。但我觉得,其实本质上来说,就是代码不够完美。
2.设备错误
某些设备出现了异常错误,例如:打印机在打印过程每纸了
3.物理限制
一般是物理硬件的限制,例如:磁盘满了,可用存储空间已被用完
4.代码错误
程序方法可能无法正确执行
举个栗子:如果某个方法不能正常的执行完所有代码之后退出方法,那我们,就可以通过另一个路径退出方法。
在这种情况下,方法并不返回任何值,而是抛出(throw)一个封装了错误信息的对象。
这时候,这个方法将会立刻退出,并不返回任何值,调用这个方法的代码也将无法继续执行了,取而代之的是,异常处理机制开始搜索能够处理这种异常状况的异常处理器(exception handler)。
3.java中的异常处理机制
对于异常情况,java使用一种称为异常处理(exception handing)的错误捕获机制处理。java中的异常处理与C++或Dephi中的异常处理机制十分相似。
为学习带来点乐趣
很恐怖的比喻😺(对应上面好吃的🌰):
有一天晚上你在一个伸手不见五指的地方敲着代码(好吧,你还有一个电脑的光,也不算伸手不见五指),敲着敲着,后面突然有个人拍了你一下,你回头看了一下,是只👻,然后你就很淡定的站起来,关了电脑,然后掉头就跑(应该拿起电脑一起跑,程序员的电脑多宝贵,万一被👻吃了咋办)。
故事就是这么个故事,你看怎么办吧!!!
接下来的代码没敲(调用这个方法的代码也将无法继续执行了),那遇到👻(异常),按道理来说要请一些道长回来抓一手(异常需要处理,需要找异常处理器),那你忘记刚刚那个👻长什么样了,那你就回去看一下👻是什么样,🧟♀️这个样子的话你就找林正英呗(报一下我名字收费打5折),那要是贞子这样滴,咋办?找个🇯🇵抓鬼大师来?我也不知道,你别不敢信,我还不敢问,还不敢说。这样找对应的人来处理(对应的异常处理器处理对应的异常)。这样就处理完👻,那你就可以愉快的写代码了。
4.异常分类
任何时候人对图像的记忆总是大于文字的,那么就来张图(java异常层次结构图)
那既然有图,那我们就加上对图的一些分析吧
分析
所有的异常对象都是由Throwable继承来的,但是在下一层立即分解为两个分支:Error和Exception
Error
该类层次结构描述java运行时系统的内部错误和资源耗尽这一类的错误。
如果出现这样的内部错误,除了通知给用户,并尽力使程序完全地终止之外,再无其他办法。
另外这种情况一般来说比较少见
Exception
设计程序的时候,我们更多的是关注Exception这个层次结构。这个层次结构又分解为两个分支(根据是否继承RuntimeException这个类来划分):一个分支派生于RuntimeException,另一个分支包含其他异常。
java语言规范将派生于Error类或RuntimeException类的所有异常称为非受查(unchecked)异常,所有其他异常称为受查(checked)异常
注:受查非受查简单来说就是java编译的时候检查代码是否报错,如果报错就说明是编译报错(在编译期受到JVM的检查),编译异常只是编译报错的一种
举个🌰:分析异常出现的过程(前提一个数组大小为2的数组)
访问数组中的3索引,而数组没有3索引,这时候JVM就会检测出程序会出现的异常
JVM会做两件事情
1.JVM会根据异常产生的原因创建一个异常对象,这个异常对象包含了异常产生的(内容,原因,位置)
new ArrayIndexOutOfBoundsException(“3”)
2.在getElement方法中,没有异常的处理逻辑(try…catch),那么JVM就会把异常对象抛出给方法的调用者main方法来处理这个异常
3.main方法接收到异常对象,也没有异常的处理逻辑,就会继续把对象抛出给main方法的调用者JVM处理
4.JVM接收到这个异常对象,做了两件事情
把异常对象(内容,原因,位置)以红色的字体打印在控制台
JVM会终止当前正在执行的java程序—>中断处理
5.异常中的关键字
try…catch
格式:
try{
可能产生异常的代码
} catch(定义一个异常的变量,用来接收try中抛出的异常对象){
异常的处理逻辑
一般会把异常的信息记录到一个日志中
} catch(异常类名 变量名) {
}
注意事项:
1.try中可能抛出多个异常对象,那么就可以使用多个catch来处理这些异常对象
2.如果try中产生了异常,那么就会执行catch中的异常处理逻辑,执行完毕catch中的处理逻辑,继续执行try…catch之后的代码
如果try中没有产生异常,那么就不会执行catch中异常的处理逻辑,执行完try中的代码,继续执行try…catch之后的代码
3.一个try多个catch注意事项
catch里边定义的异常变量,如果有子父类关系,那么子类的异常变量必须写在上边,否则就会报错(因为你如果把父类的异常变量写在上边的话,写在下边的子类异常变量就永远无法执行到,java语言是不运行永远不能执行到的代码存在。这就好比你在return子句后面又写了代码,这个时候程序也会报错,因为return代表方法结束了,写在return子句后面的代码永远不可能执行到,所以程序就会报错)
try中如果出现了异常对象,会把异常对象抛出给catch处理,抛出的异常对象,会从上到下依次赋值给catch中定义的异常变量
finally
格式:
try{
可能产生异常的代码
} catch(定义一个异常的变量,用来接收try中抛出的异常对象){
异常的处理逻辑
一般会把异常的信息记录到一个日志中
} catch(异常类名 变量名) {
}finally{
无论是否出现异常都会执行
}
注意事项:
1.finally不能单独使用,必须和try一起使用
2.finally一般用于资源释放(资源回收),无论程序是否出现异常,最后都要资源释放(IO)
throw
使用throw关键字在指定的方法中抛出指定的异常,格式:
throw new xxxException("异常产生的原因")
注意事项:
1.thorw关键字必须写在方法的内部
2.throw关键字后边的new的对象必须是Exception或者Exception的子类对象
3.throw关键字抛出指定的异常对象,我们必须要处理这个异常对象
如果创建的是RuntimeException或者是RuntimeException的子类对象,我们可以不处理,默认交给JVM处理(打印异常对象,中断程序)
如果创建的是编译异常(写代码的时候报错),我们必须处理这个异常,要么throws要么try…catch
throws
当方法内部抛出异常对象的时候,我们就必须处理这个异常对象
可以使用throws关键字处理异常对象,会把异常对象声明抛出给方法的调用者处理(自己不处理,给比人处理)最终交给JVM处理—>中断处理
在方法声明时使用,格式:
修饰符 返回值类型 方法名(参数列表) throws AAAException,BBBException...{
throw new AAAException("产生原因");
throw new BBBException("产生原因");
}
注意事项
1.throws关键字必须写在方法声明处
2.throws关键字后边声明的异常必须是Exception或者Exception的子类
3.方法内部如果抛出了多个异常对象,那么throws后边必须也声明多个异常,
如果抛出多个异常有子父类关系,那么直接声明父类异常即可
4.调用了一个声明抛出异常的方法,我们必须处理声明的异常
要么继续使用throws声明抛出,交给方法的调用者处理,最终交给了JVM
要么try…catch自己处理异常
问题
1.为什么编译时异常我们必须需要处理
答:你知道查看对应异常的源码即可知道这个结果,因为这些异常它们会向上抛出异常,那只能自我们这边调用方进行处理了
public FileInputStream(String name) throws FileNotFoundException {
this(name != null ? new File(name) : null);
}
分析:
这个是FileInputStream构造方法的源码,这里就抛出了FileNotFoundException,所以我们在调用public FileInputStream(String name)这个方法的时候必须处理这个异常
比喻:
工作中交给你一个任务,但你这个任务没办法完成,那你就把这个任务告诉上司,那上司就得处理这个任务,要么告诉他的上司,要么他自己进行处理
2.throws和throw关键字的区别
throw可以理解为抛出单个异常,比如在方法内部遇到了异常(一次肯定是遇到一个问题),所以这个时候异常数量是单数,所以用throw,将这个异常丢给这个方法(主体是方法,先丢给方法本身)。
那throws关键字可以理解为一次性抛出多个异常,就是你这个方法里面可能遇到了很多问题(异常),这些异常都被收集起来了,然后一次性都交给调用者去处理
6.自定义异常类
我们的目的是自定义异常类,那我们就得先去看看java里面异常类的特点
public class FileNotFoundException extends IOException
public class IOException extends Exception
public class NullPointerException extends RuntimeException
我随便查了几个放在这里,从这可以看出几点
1.java异常类一般以Exception结尾,以此说明该类是一个异常类
2.java异常类最终都继承Exception类(注:RuntimeException是Exception的子类,因为java中划分运行时异常与其他异常是以是否继承RuntimeException为标准的)
那么我们可以来得知自定义类的情况如下
自定义类格式:
public class XXXException extends Exception | RuntimeException{
添加一个空参数的构造方法
添加一个带异常信息的构造方法
}
注意
1.自定义异常类一般都是以Exception结尾,说明该类是一个异常类
2.自定义异常类,必须继承Exception | RuntimeException
继承Exception:那么自定义的异常类就是一个编译期异常,如果方法内部抛出了编译期异常,就必须处理这个异常,要么throws,要么try…catch
继承RuntimeException:那么自定义的异常类就是一个运行期异常,无需处理,交给虚拟机处理(中断异常)
异常使用准则
(以下来自《Effective Java》一书学习整理)
1.只针对异常的情况才使用异常
异常是为了在异常情况下使用而设计的
try {
Person[] range = new Person[10];
int i = 0;
while(true){
range[i++].method()
}
} catch (ArrayIndexOutOfBoundsException e) {
//异常处理
}
分析上面这个代码,这个代码是在i++=10的时候就会报ArrayIndexOutOfBoundsException这个异常,但是这个异常我们一般可以使用代码直接避免,例如
for(int i = 0;i < arr.length; i++) {
}
这种情况我们就没必要再去做一个异常的处理了,直接避免就行了
基于异常处理的循环不仅会模糊代码的意图,还会降低它的性能,而且还不能保证代码正常工作
1>异常应该只用于异常的情况,永远不应该用于正常的控制流
2>设计良好的API不应该强迫它的客户端为了正常的控制流而使用异常
2.对可恢复的情况使用受检异常,对编程错误使用运行时异常
1>在决定使用受检异常还是非受检异常时,主要的原则是:如果期望调用者能够适当的恢复,对于这种情况就应该使用受检异常
2>用运行时异常来表明编程错误,例如:NullPointException
3>实现的所有未受检的抛出结构都应该是RuntimeException的子类(直接或间接)
3.避免不必要地使用受检异常
在谨慎使用的前提之前,受检异常可以提升程序的可读性;如果过度使用,将会使API使用起来非常痛苦
如果调用者无法恢复失败,就应该抛出未受检异常;
如果可以恢复,并且想要迫使调用者处理异常的条件,首选应该返回一个option值。
当且仅当万一失败时,这些无法提供足够的信息,才应该抛出受检异常
4.优先使用标准的异常
1>不要直接重用Exception、RuntimeException、Throwable或者Error,对待这些类要像对待抽象类一样,因为他们是一个方法可能抛出的其他异常的超类,所以我们无法可靠的测试这些异常
5.抛出与抽象对应的异常
1>更高层的实现应该捕获低层的异常,同时抛出可以按照高层抽象进行解释的异常(这种做法称为异常转译exception translation)
2>尽管异常转译与不加选择地从低层传递异常的做法相比有所改进,但是也不能滥用它
6.每个方法抛出的所有异常都要建立文档
1>描述一个方法所抛出的异常,是正确使用这个方法时所需文档的重要组成部分。所以我们始终要单独地声明受检异常,准确地记录下抛出每个异常的条件
2>使用javadoc的@throws标签记录下一个方法可能抛出的每个未受检异常,但是不要使用throws关键字将未受检的异常包含在方法的声明中
3>如果一个类中许多方法出于同样的原因而抛出同一个异常,那么在该类的文档注释中对这个异常建立文档,是可以接受的
7.在细节消息中包含失败-捕获信息
1>就是说在catch的时候,异常的信息应该包含“对该异常有贡献”的所有参数和域的值,这样的话你就能通过这些异常信息知道更快速的知晓异常发生的原因
2>细节消息中不能包含一些隐秘的信息,例如密码、秘钥之类的,防止一些重要的信息泄露
8.努力使失败保持原子性
1>失败的方法调用应该使对象保持在被调用之前的状态。具有这种属性的方法被称为具有失败原子性
其实这里我联想到的是事务,就是方法出错了,那就回滚,滚到执行方法之前的状态,感觉跟这个失败原子性有点相似
9.不要忽略异常
try {
...
} catch (SomeException e) {
// 如果一定要忽略异常说明为什么可以这样做,并且应该将SomeException的变量e命名为ignored
}
1>空的catch块会使异常达不到应有的目的(即直接捕获了异常,但是什么都不告诉调用者,这样调用者自然就不知道这里出现了异常)
2>但是如果一定选择忽略异常的话,catch块中应该写明注释包含为什么可以这样做,以及需要将变量命名为ignored
努力终将有所收获,时间长短而已,请坚持下去。愿你成为太阳🌞,收下向日葵🌻