前面介绍的几种异常(不包含错误),编码的时候没认真看还发现不了,直到程序运行到特定的代码跑不下去了,程序员才会恍然大悟:原来这里的代码逻辑有问题。像这些在运行的时候才暴露出来的异常,又被称做“运行时异常”,与之相对的另外一类异常叫作“非运行时异常”。所谓非运行时异常,指的是在编码阶段就被编译器发现这里存在潜在的风险,须要开发者关注并加以处理。好比把某个字符串转换成日期类型,用到了SimpleDateFormat实例的parse方法,假若按照常规方式编码,则编译器会在parse这行提示代码错误,并给出以下图所示的处理建议小窗。html
可见编译器提供了两种解决办法,第一种是“Add throws declaration”,表示要添加throws声明;第二种是“Surround with try/catch”,表示要用try/catch语句把parse行包围起来。为了消除编译错误,姑且先采用第一种解决方式,给parse行所在的方法添加“ throws ParseException”,下面是修改后的演示代码:java
// 解析异常:指定日期不是真实的日子 // ParseException属于编译时异常,在编码时就要处理,不然没法编译经过。 // 处理方式有两种:一种是往外丢异常,另外一种是经过try...catch...语句捕捉异常 private static void getDateFromFormat() throws ParseException { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); String strDate = "2021/02/28"; Date date = sdf.parse(strDate); }
然而不光是上面的getDateFromFormat方法须要添加throws声明,连该方法所在的main方法也要添加throws声明才行。好不容易把该加的throws语句全都加了,接着故意填个格式错误的日期字符串,运行这个格式转换代码,果真程序输出了异常信息“java.text.ParseException: Unparseable date: "2021/02/28"”。
不过手工添加throws实在麻烦,得从调用parse的地方开始一层一层往上加过去,改动量太大。那么再试试编译器提供的第二种解决方式,也就是parse这行增长try/catch语句块,具体代码示例以下:程序员
// 经过try...catch...语句捕捉日期的解析异常 private static void getDateWithCatch() { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); String strDate = "2021/02/28"; try { // 开始当心翼翼地尝试,随时准备捕捉异常 Date date = sdf.parse(strDate); } catch (ParseException e) { // 捕捉到了解析异常 e.printStackTrace(); // 打印出错时的栈轨迹信息 } }
运行包含try/catch的以上代码,程序依然打印ParseException的相关异常日志,只是此时的打印动做由catch内部的“e.printStackTrace();”触发。但这不是重点,重点在于try与catch两个代码块之间的关系。从示例代码可知,try后面放的是普通代码,而catch后面放的是异常信息打印语句,它们对应着两个分支:一个try正常分支,一个catch异常分支。若是try的内部代码彻底正常运行,则异常分支的内部代码根本不会执行;若是try的内部代码运行出错,则程序略过try的剩余代码,直接跳到异常分支处理。照这么看,try/catch的处理逻辑相似于if/else,都存在“若是……就……,不然……”的分支操做。不一样之处在于,try语句并不指定知足进入的条件,而是由程序在运行时根据是否发生异常来决定继续处理仍是跳到异常分支。何况也不是全部的异常都能跳进catch分支,只有符合catch语句指定的异常种类,才能跳的进去,不然仍是往上一层一层扔出异常了。
有了try和catch这对好搭档,程序运行时不论是正常分支仍是异常分支都可妥善处理了。不过有的业务须要在操做开始前分配资源,在操做结束后释放资源,例如访问数据库就得先创建数据库链接,再进行记录的增删改查等操做,最后处理完了再释放数据库链接。对于这种业务,不管是正常流程仍是异常流程,最终都得执行资源释放操做。或许有人说,在try/catch整块代码后面补充释放资源不就得了?要是针对if/else的业务场景,却是能够这么干;但如今业务场景变成try/catch,就不能如此蛮干了。由于在try/catch整块后面添加代码,新代码本质上仍走正常流程,即try/catch两个分支并流以后的正常流程。同时catch语句只能捕捉到某种类型的异常,并不能捕捉到全部异常,也就是说,一旦try内部遇到了未知异常,这个未知异常不会跳到现有的catch分支(因catch分支没法识别未知异常),而是当场一层一层往外扔出未知异常。这样一来,跟在try/catch后面的资源释放代码根本没机会执行,故而该方式将在遇到未知异常时失效。
为了保证在全部状况下(没有异常,或者遇到任何一种异常包括未知异常)都能执行某段代码,Java给try/catch机制增长了finally语句,该语句要求程序无论发生任何状况都得进来到此一游,像资源释放这种代码就适合放在finally内部,管你没异常仍是有异常仍是什么未知异常,最终通通拉到finally语句里面走一遭。仍以日期转换为例,要求给某个字符串形式的日期加上若干天,若是字符串日期解析失败,则自动用当前日期代替,而且不管遇到什么异常,务必返回一个正常的日期字符串。据此联合运用try/catch/finally,编写出来的处理代码以下所示:数据库
// 给指定日期加上若干天。若是日期解析失败,则自动用当前日期代替 private static String addSomeDays(String strDate, int number) { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); Date date = null; try { // 开始当心翼翼地尝试,随时准备捕捉异常 date = sdf.parse(strDate); } catch (ParseException e) { // 捕捉到了解析异常 date = new Date(); } finally { // 不管是否发生异常,都要执行最终的代码块 if (date == null) { date = new Date(); } long time = date.getTime() + number*24*60*60*1000; date.setTime(time); } return sdf.format(date); }
这下总算实现了任意状况都可正常运行的需求,try/catch/finally三兄弟联手,正应了那句老话“三个臭皮匠,胜过诸葛亮”。数组
除了系统自带的各类异常,程序员也可本身定义新的异常,自定义异常很简单,只需从Exception派生出子类,并编写该类的构造方法便可。下面即是两个自定义异常的代码例子,第一个是数组为空异常,定义代码以下所示:测试
//定义一个数组为空异常。异常类必须由Exception派生而来 public class ArrayIsNullException extends Exception { private static final long serialVersionUID = -1L; public ArrayIsNullException(String message) { super(message); } }
第二个是数组越界异常,定义代码以下所示:编码
//定义一个数组越界异常。异常类必须由Exception派生而来 public class ArrayOutOfException extends Exception { private static final long serialVersionUID = -1L; public ArrayOutOfException(String message) { super(message); } }
因为这两个是自定义的异常,不会被系统自动丢出来,所以须要由程序员在代码中手工扔出自定义的异常。扔出异常的代码格式为“throw 某异常的实例;”,异常扔出以后,假若当前方法没有捕捉异常,则该方法还得在入参列表以后添加语句“throws 以逗号分隔的异常列表”,表示本方法处理不了这些异常,请求上级方法帮忙处理。举个根据下标获取数组元素的例子,正常获取指定下标的元素有两个前提:其一数组不能为空,其二下标不能超出数组范围。若是发现目标数组为空,就令代码扔出数组为空异常ArrayIsNullException;若是发现下标不在合法的位置,就令代码扔出数组越界异常ArrayOutOfException。按此思路编写的方法代码示例以下:日志
// 根据下标获取指定数组对应位置的元素 private static int getItemByIndex(int[] array, int index) throws ArrayIsNullException, ArrayOutOfException { // 同时扔出了多个异常 if (array == null) { // 若是数组为空 // 就扔出数组为空异常 throw new ArrayIsNullException("这是个空数组"); } else if (index<0 || index>=array.length) { // 若是下标超出了数组范围 // 就扔出数组越界异常 throw new ArrayOutOfException("下标超出了数组范围"); } return array[index]; }
特别注意上面的异常扔出操做用到了两个关键字,一个是没带s的throw,另外一个是带s尾巴的throws,它们之间的区别不只仅是调用位置不一样,并且一次扔出的异常数量也不一样,throw每次只能扔出一个异常,而throws容许一次性扔出多个异常。
另外,刚才的getItemByIndex方法扔出了两个异常,留待它的上级方法接手烂摊子。上级方法当然能够沿用try/catch语句捕捉异常,不过此次面对的是两个异常不是单个异常,这也好办,既然有两个异常就写上两个异常分支呗,两个catch分支分别捕捉数组为空异常和数组越界异常。如此一来,上级方法的异常捕捉代码就变成下面这般:orm
// 进行数组的下标访问测试(数组为空) private static void testArrayByIndexWithNull() { int[] array = null; try { // 开始当心翼翼地尝试,随时准备捕捉异常 // 根据下标获取指定数组对应位置的元素 int item = getItemByIndex(array, 3); System.out.println("item="+item); } catch (ArrayIsNullException e) { // 捕捉到了数组为空异常 e.printStackTrace(); // 打印出错时的栈轨迹信息 } catch (ArrayOutOfException e) { // 捕捉到了下标越界异常 e.printStackTrace(); // 打印出错时的栈轨迹信息 } }
看起来,catch分支仿佛if/else语句里的else分支,都支持有多路的条件分支。当多个else分支的处理代码保持一致时,则容许经过或操做将它们合并为一个else分支;同理,假如多个catch分支的异常处理没有差异,也支持引入或操做将它们合并为一个catch分支,具体写法形如“catch (异常A | 异常B e)”。合并异常分支以后的异常处理代码以下所示:htm
// 进行数组的下标访问测试(下标越界) private static void testArrayByIndexWithOut() { int[] array = {1, 2, 3}; try { // 开始当心翼翼地尝试,随时准备捕捉异常 // 根据下标获取指定数组对应位置的元素 int item = getItemByIndex(array, 3); System.out.println("item="+item); } catch (ArrayIsNullException | ArrayOutOfException e) { // 捕捉到了数组为空异常或下标越界异常 e.printStackTrace(); // 打印出错时的栈轨迹信息 } }
由于ArrayIsNullException和ArrayOutOfException都是Exception的子类,因此“ArrayIsNullException | ArrayOutOfException”能够被“Exception”所取代,进一步简化后的方法代码以下:
// 进行数组的下标访问测试(捕获全部异常) private static void testArrayByIndexWithAny() { int[] array = null; try { // 开始当心翼翼地尝试,随时准备捕捉异常 // 根据下标获取指定数组对应位置的元素 int item = getItemByIndex(array, 3); System.out.println("item="+item); } catch (Exception e) { // 捕捉到了任何一种异常 e.printStackTrace(); // 打印出错时的栈轨迹信息 } }
上述代码里的异常分支“catch (Exception e)”表示将捕捉任何属于Exception类型的异常,这些异常包括Exception自身及其派生出来的全部子类,固然也包含前面自定义的ArrayIsNullException和ArrayOutOfException了。
更多Java技术文章参见《Java开发笔记(序)章节目录》