前面介绍的几种异常(不包含错误),编码的时候没认真看还发现不了,直到程序运行到特定的代码跑不下去了,程序员才会恍然大悟:原来这里的代码逻辑有问题。像这些在运行的时候才暴露出来的异常,又被称作“运行时异常”,与之相对的另一类异常叫做“非运行时异常”。所谓非运行时异常,指的是在编码阶段就被编译器发现这里存在潜在的风险,需要开发者关注并加以处理。比如把某个字符串转换成日期类型,用到了SimpleDateFormat实例的parse方法,倘若按照常规方式编码,则编译器会在parse这行提示代码错误,并给出如下图所示的处理建议小窗。

可见编译器提供了两种解决办法,第一种是“Add throws declaration”,表示要添加throws声明;第二种是“Surround with try/catch”,表示要用try/catch语句把parse行包围起来。为了消除编译错误,姑且先采用第一种解决方式,给parse行所在的方法添加“ throws ParseException”,下面是修改后的演示代码:

	// 解析异常:指定日期不是真实的日子
// 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分支分别捕捉数组为空异常和数组越界异常。如此一来,上级方法的异常捕捉代码就变成下面这般:

	// 进行数组的下标访问测试(数组为空)
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)”。合并异常分支之后的异常处理代码如下所示:

	// 进行数组的下标访问测试(下标越界)
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开发笔记(序)章节目录

Java开发笔记(七十五)异常的处理:扔出与捕捉的更多相关文章

  1. Java开发笔记(十五)短路逻辑运算的优势

    前面提到逻辑运算只能操作布尔变量,这其实是不严谨的,因为经过Java编程实现,会发现“&”.“|”.“^”这几个逻辑符号竟然可以对数字进行运算.譬如下面的代码就直接对数字分别开展了“与”.“或 ...

  2. Java开发笔记(一百五十)C3P0连接池的用法

    JDBC既制定统一标准兼容了多种数据库,又利用预报告堵上了SQL注入漏洞,照理说已经很完善了,可是人算不如天算,它在性能方面不尽如人意.问题出在数据库连接的管理上,按照正常流程,每次操作完数据库,都要 ...

  3. Java开发笔记(九十五)NIO配套的文件工具Files

    NIO不但引进了高效的文件通道,而且新增了更加好用的文件工具家族,包括路径组工具Paths.路径工具Path.文件组工具Files.先看路径组工具Paths,该工具提供了静态方法get,输入某个文件的 ...

  4. Java开发笔记(一百五十一)Druid连接池的用法

    C3P0连接池自诞生以来在Java Web领域反响甚好,业已成为hibenate框架推荐的连接池.谁知人红是非多,C3P0在大型应用场合中暴露了越来越多的局限性,包括但不限于下列几点:1.C3P0管理 ...

  5. Java学习笔记(十五)——javadoc学习笔记和可能的注意细节

    [前面的话] 这次开发项目使用jenkins做持续集成,PMD检查代码,Junit做单元测试,还会自动发邮件通知编译情况,会将javadoc生成的文档自动发到一个专门的服务器上面,每个人都可以看,所以 ...

  6. Java开发学习(二十五)----使用PostMan完成不同类型参数传递

    一.请求参数 请求路径设置好后,只要确保页面发送请求地址和后台Controller类中配置的路径一致,就可以接收到前端的请求,接收到请求后,如何接收页面传递的参数? 关于请求参数的传递与接收是和请求方 ...

  7. Java开发笔记(十九)规律变化的for循环

    前面介绍while循环时,有个名叫year的整型变量频繁出现,并且它是控制循环进出的关键要素.不管哪一种while写法,都存在三处与year有关的操作,分别是“year = 0”.“year<l ...

  8. Java学习笔记二十五:Java面向对象的三大特性之多态

    Java面向对象的三大特性之多态 一:什么是多态: 多态是同一个行为具有多个不同表现形式或形态的能力. 多态就是同一个接口,使用不同的实例而执行不同操作. 多态性是对象多种表现形式的体现. 现实中,比 ...

  9. Android笔记(七十五) Android中的图片压缩

    这几天在做图记的时候遇第一次遇到了OOM,好激动~~ 追究原因,是因为在ListView中加载的图片太大造成的,因为我使用的都是手机相机直接拍摄的照片,图片都比较大,所以在加载的时候会出现内存溢出,那 ...

  10. 树莓派开发笔记(十五):树莓派4B+从源码编译安装mysql数据库

    前言   树莓派使用数据库时,优先选择sqlite数据库,但是sqlite是文件数据库同时仅针对于单用户的情况,考虑到多用户的情况,在树莓派上部署安装mysql服务,通过读写锁事务等使用,可以实现多进 ...

随机推荐

  1. 判断js中的数据类型的几种方法

    判断js中的数据类型有一下几种方法:typeof.instanceof. constructor. prototype. $.type()/jquery.type(),接下来主要比较一下这几种方法的异 ...

  2. ndk编译ffmpeg

    #!/bin/bash NDK=/opt/android-ndk-r9d SYSROOT=$NDK/platforms/android-9/arch-arm/ TOOLCHAIN=$NDK/toolc ...

  3. [Java]LeetCode278. 第一个错误的版本 | First Bad Version

    You are a product manager and currently leading a team to develop a new product. Unfortunately, the ...

  4. [Swift]LeetCode862. 和至少为 K 的最短子数组 | Shortest Subarray with Sum at Least K

    Return the length of the shortest, non-empty, contiguous subarray of A with sum at least K. If there ...

  5. TCP的三次握手与四次挥手(个人总结)

    序列号seq:占4个字节,用来标记数据段的顺序,TCP把连接中发送的所有数据字节都编上一个序号,第一个字节的编号由本地随机产生:给字节编上序号后,就给每一个报文段指派一个序号:序列号seq就是这个报文 ...

  6. 定时任务 winform开发

    在项目中我们经常遇到与时间结合的无限或者有限轮回的任务.例如每月一号统计工作量,基本这种情况,都会是设置定时任务,定时执行.好了,下面就记录一下定时任务的开发吧. 首先描述一下开发思路: 建立一个wi ...

  7. 14.Git分支-rebase有趣的例子、变基带来的问题及解决方案

    1.有趣的变基例子 如下图所示,你创建了一个特性分支server,然后进行了一些提交(C3和C4),然后又从C3上创建了特性分支client,提交了C8和C9,最后你又回到了server,提交了C10 ...

  8. 【Spark篇】---Spark中资源和任务调度源码分析与资源配置参数应用

    一.前述 Spark中资源调度是一个非常核心的模块,尤其对于我们提交参数来说,需要具体到某些配置,所以提交配置的参数于源码一一对应,掌握此节对于Spark在任务执行过程中的资源分配会更上一层楼.由于源 ...

  9. BBS论坛(三十二)

    32.帖子排序功能完成 (1)front_index.html <ul class="post-group-head"> {% if current_sort==1 % ...

  10. 并发编程(十六)——java7 深入并发包 ConcurrentHashMap 源码解析

    以前写过介绍HashMap的文章,文中提到过HashMap在put的时候,插入的元素超过了容量(由负载因子决定)的范围就会触发扩容操作,就是rehash,这个会重新将原数组的内容重新hash到新的扩容 ...