概述
顾名思义,通俗来讲异常就是指,那些发生在我们原本考虑和设定的计划之外的意外情况。
生活中总是会存在各种突发情况,如果没有做好准备,就让人措手不及。
你和朋友约好了明天一起去登山,半道上忽然乌云蔽日,下起了磅礴大雨。这就是所谓的异常情况。
你一下子傻眼了,然后看见朋友淡定的从背包里掏出一件雨衣穿上,淫笑着看着你。这就是对异常的处理。
对于一个OO程序猿来讲,所做的工作就是:将需要处理的现实生活中的复杂问题,抽象出来编写成为程序。
既然现实生活中总是存在着各种突然的异常情况,那么对应其抽象出的代码,自然也是存在这样的风险的。
所以常常说:要编写一个完善的程序并不只是简简单单的把功能实现,还要让程序具备处理在运行中可能出现的各种意外情况的能力。
这就是所谓的异常的使用。
体系
这就是Java当中异常体系的结构构成,从图中我们可以提取到的信息就是:
1、Java中定义的所有异常类都是内置类Throwable的子类。
2、Java中的异常通常被分为两大类:Error和Exception:
- 那么顾名思义,Error代表错误,Exception代表异常;
- Error用以指明与运行环境相关的错误,JVM无法从此类错误中恢复,此类异常无需我们处理;
- Exception代表着可以被我们所处理的异常情况,我们需要掌握和使用的,正是该类型。
3、Exception最常见的两种异常类型分别是:
- IOException:主要是用以处理操作数据流时可能会出现的各种异常情况。
- RuntimeException:指发生在程序运行时期的异常,如数组越界,入参不满足规范等情况引起的程序异常。
工欲善其事,必先利其器
要理解Java中的异常使用,首先要明白几个关于异常处理的工具 - 异常处理关键字的使用。
1、throw:用以在方法内部抛出指定类型的异常。
void test(){ if(发生异常的条件){ thorw new Exception("抛出异常"); } }
2、throws:用以声明 一个方法可能发生的异常(通常都是编译时检测异常)有哪些。
void test() throws 异常1,异常2{ //这个方法可能发生异常1,异常2 throw new 异常1(); throw new 异常2(); }
3、try - catch:另一种处理异常的方式,与throws不同的是,这种方式是指 "捕获并处理"的方式。
用try语句块包含可能发生异常的代码,catch用于捕获发生的异常,并在catch语句块中定义对捕获到的异常的处理方式。
try { //可能发生异常的代码 } catch (要捕获的异常类型 e) { //对捕获到的异常的处理方式 }
4、finally语句块:通常都是跟在try或try-catch之后进行使用,与其名字代表的一样。也就是定义最终的操作。
特点是被try语句块包含的内容中,是否真的发生了异常。程序最终都将执行finally语句块当中的内容。
通常用于对资源的释放操作,例如:通过JDBC连接数据库等情况。
try { //获取资源 } catch (要捕获的异常类型 e) { //对捕获到的异常的处理方式 }finally{ //释放资源 }
趣解异常的实际使用
了解Java中异常类的实际应用之前,应当先了解两个概念,用以对最常用的异常做一个分类:
1、编译时被检测异常:
只要是Exception和其子类都是,除了特殊子类RuntimeException体系。
所谓的编译时被检测异常也就是指在程序的编译器就会进行检测的异常分类。
也就是说,如果一个方法抛出了一个编译时检测异常,Java则要求我们必须进行处理。
既:通过throws对异常进行声明处理 或是 通过try-catch对异常进行捕获处理。
如果程序编译时检测到该类异常没有被进行任何处理,那么编译器则会报出一个编译错误。
public class Test{ public static void main(String[] args) { try { Class clazz = Class.forName("Java"); System.out.println(clazz.getName()); } catch (ClassNotFoundException e) { System.out.println("没有找到该类"); } } }
上面代码中的ClassNotFoundException就是一种编译时检测异常,这个异常是由Class类当中的forName方法所抛出并声明的。
如果我们在使用该方法时没有对异常进行处理:声明或捕获,那么该程序就会编译失败。
通过这个例子想要说明的是:编译时被检测异常通常都是指那些“可以被我们预见”的异常情况。
正例如:我们通过Class.forName是想要获取指定类的字节码文件对象,所以我们自然也可以预见可能会存在:
与我们传入的类名参数所对应的类字节码文件对象不存在,查找不到的情况。
既然这种意外情况是可以被预见的,那自然就应该针对其制定一些应对方案。
2、编译时不检测异常(运行时异常):
就是指Exception下的子类RuntimeException和其子类。
通常这种问题的发生,会导致程序功能无法继续、运算无法进行等情况发生;
但这类异常更多是因为调用者的原因或者引发了内部状态的改变而导致的。
所以针对于这种异常,编译器不要求我们处理,可以直接编译通过。
而在运行时,让调用者调用时的程序强制停止,从而让调用者对自身的代码进行修正。
曾经看到过一道面试题:列出你实际开发中最常见的五个运行时异常,就我自己而言,如果硬要说出五个,那可能是:
NullPointerException(空指针异常)、IndexOutOfBoundsException(角标越界异常)、ArithmeticException(异常运算条件异常)
ClassCastException(类型转换异常)、IllegalArgumentException(非法参数异常)
public class Test{ public static void main(String[] args) { division(5, 0); } static int division(int a ,int b){ return a/b; } } /* Exception in thread "main" java.lang.ArithmeticException: / by zero at com.tsr.j2seoverstudy.base.Test.division(Test.java:31) at com.tsr.j2seoverstudy.base.Test.main(Test.java:28) */
上面的例子就报出了运行时异常:ArithmeticException。因为我们将非法的被除数0作为参数传递给了除法运算的函数内。
同时也可以看到,虽然“division”方法可能引发异常,但因为是运行时异常,所以即使不做任何异常处理,程序任然能够通过编译。
但当该类型的异常真的发生的时候,调用者运行的程序就会直接停止运行,并输出相关的异常信息。
通过自定义异常理解检测异常和非检测异常
前面我们说到的都是Java自身已经封装好提供给我们的一些异常类。由此我们可以看到,秉承于“万物皆对象”的思想,Java中的异常实际上也是一种对象。
所以自然的,除了Java本身提供的异常类之外,我们也可以根据自己的需求定义自己的异常类。
这里我想通过比较有趣的简单的自定义异常,结合自己的理解,总结一下Java当中检测异常和非检测异常的使用。
1、编译时检测异常
对于编译时异常,我的理解就是:所有你可以预见、并且能够做出应对的意外状况,都应该通过编译时检测异常的定义的方式进行处理。
举个例子来说:假定我们开了一家小餐馆,除开正常营业的流程之外。自然可能发生一些意外状况,例如:
菜里不小心出现了虫子,出现了头发;或者是餐馆突然停电之类的状况。这些状况是每个经营餐馆的人事先都应该考虑到的情况。
既然我们已经考虑到了这些意外情况发生的可能性,那么自然就应该针对于这些状况做出应对的方案。所以代码可能是这样的:
1、首先,定义两个编译时检测异常类,菜品异常和停电异常:
package com.tsr.j2seoverstudy.exception_demo; /* * 菜品异常 */ public class DishesException extends Exception{ public DishesException() { super("菜品有问题.."); } } package com.tsr.j2seoverstudy.exception_demo; /* * 停电异常 */ public class PowerCutException extends Exception{ PowerCutException(){ super("停电异常.."); } }
2、然后在餐厅类当中,对异常作出处理:
package com.tsr.j2seoverstudy.exception_demo; public class MyRestaurant { private static String sicuation; static void doBusiness() throws DishesException, PowerCutException{ if(sicuation.equals("菜里有虫") ||sicuation.equals("菜里有头发")){ throw new DishesException(); } else if(sicuation.equals("停电")){ throw new PowerCutException(); } } public static void main(String[] args) { try { doBusiness(); } catch (DishesException e) { //换一盘菜或退款 } catch (PowerCutException e) { //启动自备发电机 } } }
1、我们已经说过了菜品出现问题和停电之类的意外情况都是我们可以预见的,所以我们首先定义了两个编译时检测异常类用以代表这两种意外情况。
2、然后我们在餐厅类当中的营业方法当中做出了声明,如果出现“菜里有虫”或“菜里有头发的问题”,我们就用thorw抛出一个菜品异常;如果“停电”,就抛出停电异常。
3、但是,由于我们抛出这一类异常是因为想告知餐厅的相关人员,在餐厅营业后,可能会出现这些意外情况。所以还应当通过throws告诉他们:营业可能会出现这些意外情况。
4、餐厅相关人员接到了声明。于是制定了方案,当餐厅开始营业后。如果出现了菜品异常,请为客人换一盘菜或退款;如果出现停电异常,请启动店里自备的发电机。
2、运行时异常
对于运行时异常的使用,我个人觉得最常用的情况有两种:
第一、编译时检测异常用于定义那些我们可以提供“友好的解决方案”的情况。那么针对于另外一些状况,可能是我们无法很好的进行解决的。
遇到这种情况,我们可能希望采取一些“强制手段”,那就是直接让你的程序停止运行。这时,就可以使用运行时异常。
第二、如果对异常处理后,又引发一连串的错误的“连锁反应”的时候。
我们先来看一下第一种使用使用情况是怎么样的。例如说:
我们在上面的餐厅的例子中,餐厅即使出现菜品异常或停电异常这一类意外情况。
但针对于这一类的意外情况,我们是能够提供较为妥善的解决方案的。
而通过我们提供的针对于这些异常情况的解决方案进行处理之后,餐厅照常营业,顾客接着用餐(程序依旧能够正常运行)。
但还有一种情况,可能无论我们怎么样友好的尝试进行解决,都难以让顾客满意。这种顾客就是传说中被称为“砸场子”的顾客。
针对于这种情况,我们可能就要采取更为“强硬的措施”了。例如直接报警把他带走(不让程序继续运行了),这就是所谓的运行时异常:
package com.tsr.j2seoverstudy.exception_demo; //砸场子异常 public class HitException extends RuntimeException { HitException() { super("草,砸场子,把你带走! "); } }
这时,餐馆类被修改为:
package com.tsr.j2seoverstudy.exception_demo; public class MyRestaurant { private static String sicuation; static void doBusiness() throws DishesException, PowerCutException { if (sicuation.equals("菜里有虫") || sicuation.equals("菜里有头发")) { throw new DishesException(); } else if (sicuation.equals("停电")) { throw new PowerCutException(); } else if (sicuation.equals("砸场子")) { throw new HitException(); } } public static void main(String[] args) { try { sicuation = "砸场子"; doBusiness(); } catch (DishesException e) { // 换一盘菜或退款 } catch (PowerCutException e) { // 启动自备发电机 } } }
于是运行该程序,就会出现:
可以看到出现该运行时异常,程序将直接被终止运行,砸场子的人直接被警察带走了。
那么接下来,我们就可以来看看第二种使用情况了,什么是所谓的“引发连锁效应的错误”。
举个例子来说,以我们上面用到的“被除数为0”的异常情况。你可能会思考:传入的被除数为0,这样的情况我们是可以考虑到的。
并且我们也可以针对这样的错误给出对应的措施。那Java为什么不将这样的异常定义为编译时检测异常呢?
那么我不妨假设ArithmeticException就是编译时检测异常,所以我们必须对其作出处理,那么可能出现这样的代码:
public class Test { public static void main(String[] args) { System.out.println("5除以0的结果为:" + division(5, 0)); } static int division(int a, int b) { int num = 0; try{ num = a/b; }catch (ArithmeticException e) { num = -1; } return num; } } }
我们提供了一个进行除法运算的方法,针对于传入的被除数为0的异常情况,我们也给出了自己的解决方案:
如果传入的被除数为0,就返回负数“-1”,“-1”就代表这个运算出错了。
于是这时有一个调用者,刚好调用了我们的除法运算方法计算“5除以0的结果”,理所当然的,他得到的结果为:
“5除以0的结果为-1”。好了,这下叼了,这哥们立马拿着这个运算结果,去向他的朋友炫耀:
你们都是2B吧,算不出5除以0等于多少是吧?告诉你们,等于-1。于是,在朋友的眼中,他成2B了。