java零基础入门-高级特性篇(八) 异常 上
在编程的过程中不可避免的出现错误,有些错误在编译时就可以发现,编程工具也会给你提示。但是有些错误只会在运行的时候才出现,但正是由于在运行时才出现的错误,会造成很严重的后果,轻则程序终止,重则系统崩溃。所以在写程序的过程中必须要尽最大可能避免出现错误,而java的异常机制则非常好的帮助我们做到这一点。
异常是什么
异常就是在程序的运行过程中,出现了意料之外的情况,导致系统出现错误。比如有一天想来场说走就走的旅行,订机票,然后要去机场赶飞机,这个就是一个完整的计划。但是,在赶飞机的过程中可能会遇上堵车,被追尾,还有可能碰到飞机延误,出现极端天气之类的情况导致旅行计划泡汤,这些就是异常了。
程序也是一样,我们原本期望程序可以不出意外的按照我们的设计要求达成任务要求,可是在程序运行后会有很多在编写程序时无法预料到的情况出现,那该怎么办?继续看旅行计划,如果能预料到可能会出现哪些异常情况,我们就可以事先做好有针对性的防范措施。
比如为了不堵车,可以选择避开上下班等高峰时段去机场,为了避免发生事故,干脆就别开车坐地铁算了,还有极端天气,可以先查查天气预报再订机票,这样就可以有效的避免旅游行程因为意外情况被终止。这种事先对有可能出现异常的情况作出针对性措施的行为,在java里叫做捕获异常,这个后面再详细介绍。
虽然可以事先对可能出现的异常作出针对性的措施,但是异常有个最大的问题就是不可穷举。也就是说不管你做再多的防范措施,也有你意想不到的情况出现。比如旅游的时候看了天气预报,避开高峰乘坐地铁,还是可能发现忘了带证件无法登机等意外情况。如果在程序中碰到这种情况,那么程序依然会由于异常而终止。所以处理异常必须要有一个完整的机制来应对。
异常是如何产生的
假如现在有一个很简单的任务,页面上面有一个输入框用来输入年龄和一个提交按钮,需要把输入的值保存到数据库中。就这么一个简单的任务,下面就开始程序员和测试的斗法了。看似简单的问题,其实包含了很多导致程序异常结束的因素。
就这么一个简单的输入年龄的输入框,如果程序没有对代码进行任何限制,可能会出现各种异常。这里接收输入框内容的应该是一个int类型的变量,但是如果输入的是字母,就会发生类型不匹配的异常,年龄是有限制的,如果输入211这种数值或者超过3位数的数字也会发生错误,不输入直接提交在对数据进行处理的时候可能会发生空指针类型异常,如果对输入可执行的代码也没有进行防范,那就不仅仅是程序异常的问题了,甚至可能导致服务器遭到攻击。
这还仅仅是单个输入框的情况,现实情况是通常会有多个输入框比如注册账号的时候需要填写多个信息,并且多个输入框之间还会有联系,那么其复杂程度会更高,出现错误的情况也会更多。
要知道一个程序能够顺利上线前是必须经过测试这一关的,而测试会模拟各种奇葩用户的行为对程序进行测试,由于用户的不规则操作会引起程序的各种错误,而测试必须事先对这些错误进行排查,保证程序不会因为用的错误输入而发生错误。
java中的异常体系
有一个很有意思的情况,当工作顺风顺水的时候,大家都是一团和气商业互吹,但是一旦出现问题,大部分人并不是找自身哪里有问题,而是急于把“锅”甩出去,相互推卸责任,“这不是我的问题!”。在吸取经验以后,这个睿智的团队为了不相互“甩锅”,在工作之前就会划分好职责范围,这样就避免出现甩锅的情况了。
java也是一样,一旦出错出异常,就开始甩锅。为了分清责任,于是java也把责任划分清楚,没有相互甩锅,排查问题定位问题会方便很多。来看看java怎么划分职责范围的。
首先是甩锅这个动作,在java里面当然不能称之为甩锅,用更加专业的词汇,这个动作叫做“抛出”。是不是很形象,所有可以甩的都是锅,出了问题要推出去,所以所有的锅都是可以甩的,放在java里面就是所有的错都是可以“抛出”的。所以将所有错误的父类定义为“Throwable”,错误都是可以抛出的,把这个锅定好以后,往下再继续分。
先来回忆下操作系统,虚拟机和程序的关系。虚拟机在运行的时候会向操作系统申请资源,比如内存,一旦虚拟机和操作系统之间的交互出了问题就是比较严重的错误了,这时候程序就可以完美的把锅甩出去,这种错误Error是与程序无关的。比如OutOfMemoryError内存溢出错误,系统分给虚拟机的内存不够了,虚拟机直接罢工不干活了,这时候程序写的好或者不好,对或者不对都不会改变这个错误,这种错误在程序的控制范围之外。一般将这种错误定义为Error,值得注意的是,由于这种错误程序无法控制,所以也无法通过程序的异常机制去堵住这个漏洞,虚拟机都罢工了你怎么写代码挽救都无济于事。
接下来就是程序自身的错误Exception了,程序自身的错误是可以通过异常机制堵住漏洞的。Exception的错误原因也很多,所以必须得继续划分错误范围,通常将Exception分为RuntimeException,SQLException,IOExcetion等,这里需要重点关注的是RuntimeException。
为什么要重点关注RuntimeException?因为在Exception中将RuntimeException定义为不检查异常,因为RuntimeException异常一般都是因为逻辑错误引起的,程序无法判断你的逻辑对不对,所以干脆不检查,当程序运行的时候发现错误了,就会报错终止程序。而除了RuntimeException异常之外的都是检查异常,在编译的时候就必须处理这类异常,比如SQLException,编译的时候要么将错误抛出去,要么自己捕获处理掉,不能啥也不干。
何为不检查异常?
RuntimeException,运行时异常,就是写代码的时候无法确认这句代码是否有错,而到了运行程序的时候,它有可能错,又有可能不错。比如初学者最容易出现没有之一的错误,NullPointerException-空指针异常。
1.首先看空指针异常出现的原因,上图中最关键的地方就是info.getInfo(),info这个对象调用了getInfo()方法,如果info是个正常的对象,无论字段是否有值,只要有初始化就可以正常调用方法,但是如果info在程序的某个地方被修改为null,那么再使用info去调用方法就会出现空指针。因为将info赋值为null后,这个变量就不再指向堆里任何一个对象,自然无法使用对象调用方法了,所以使用null调用任何方法都会出现空指针异常。
2.再来看为何程序不检查异常。因为程序的结果是由逻辑决定的,可能正确也可能出现异常,所以在编写程序的时候程序无法判断是否需要检查异常,所以就不需要检查异常。那么这种不需要检查的异常出现了该怎么办?当然是修改程序啊,这是逻辑错误,也是造成BUG的主要原因,将逻辑修改成正确没有漏洞的即可,一定要确保不会出现此类异常。比如这里如果需要将程序修改成没有异常出现的情况,就要使info的值在任何时候都不要被赋值为null即可(即在偶数时也返回一个通过new创建出来的ErrorInfo对象)。
那么检查异常是什么?检查异常就是必须在编译阶段处理的异常,不处理不给编译通过。
这里无需关系代码内容,这是IO流的知识,这里只是演示检查类型异常的情况。
总结一下,Error错误代码无法处理,Exception中的RuntimeException无需处理,一旦发生必须修改代码逻辑查缺补漏。而剩下的检查类型异常就是我们最需要关注的了,这是需要手动处理掉的异常,比如上图中工具就给我们提示,要么就地正法此异常,在这里捕获并处理掉,要么甩锅出去,抛出异常,具体操作下节继续。