Java开发笔记(七十六)如何预防异常的产生

每个程序员都希望自己的程序稳定运行,不要隔三岔五出什么差错,可是程序运行时冒出来的各种异常着实烦人,令人不胜其扰。虽然可以在代码中补上try/catch语句捕捉异常,但毕竟属于事后的补救措施。与其后知后觉地亡羊补牢,不如一开始就未雨绸缪,只要防患于未然,必能收到事半功倍的成效。
就编码时的常见异常而言,绝大多数异常都能通过适当的校验加以规避,也就是事先指定可让程序正常运行的合法条件,只有条件满足才开展业务逻辑处理,否则进行失败情况的处理。这样用于异常捕捉的try/catch语句便转换为了条件分支的if/else语句,对于熟能生巧的if/else流程控制,想必程序员在编码时更能游刃有余。接下来以几个常见的异常为例,阐述一下如何预防这些异常的发生。
首先看最简单的算术异常,如果是除数为零的异常,检查一下除数的值是否为零就行了。如果是大小数除法运算遇到的“商为无限循环小数”异常,就得在调用divide方法之时指定本次除法运算的小数精度,以及精度范围最后一位数字的舍入方式。下面是优化后的大小数除法代码例子:

	// 测试算术异常:商是无限循环小数
private static void testDivideByDecimal() {
BigDecimal one = BigDecimal.valueOf(1);
BigDecimal three = BigDecimal.valueOf(3);
// 大小数的除法运算,小数点后面保留64位,其中最后一位做四舍五入
BigDecimal result = one.divide(three, 64, BigDecimal.ROUND_HALF_UP);
System.out.println("sqrt result=" + result);
}

其次看数组越界异常,不管是根据下标访问数组元素,还是根据索引访问清单元素,都要保证待访问元素的下标必须落在数组内部(或索引落在清单内部)。因此,合法的下标数值应当大于零,且小于数组的长度,于是访问数组元素的代码可改写如下:

	// 测试越界异常:下标超出数组范围
private static void testArrayByIndex() {
int[] array = { 1, 2, 3 };
// 在根据下标获取数组元素之前,先判断该下标是否落在数组范围之内
if (array.length > 3) {
int item = array[3];
System.out.println("array item=" + item);
} else {
System.out.println("array's length isn't more than 3");
}
}

再次看空指针异常,无论是访问某对象的实例属性,还是调用某对象的实例方法,都要求该对象是真切存在着的,否则面对一个空指针瞎指挥,只能落得竹篮打水一场空的境遇了。在Java编程中,可比较某对象是否等于null来判断它是否为空指针,这样添加了空指针校验的对象访问代码示例如下:

	// 测试空指针异常:对象不存在
private static void testStringByNull() {
String str = null;
// 在跟某位对象约会之前,先打个电话问问有没空,否则牵肠挂肚空欢喜一场
if (str != null) {
int length = str.length();
System.out.println("str length=" + length);
} else {
System.out.println("str is null");
}
}

另外还有类型转换异常,因为一个父类会衍生出许多子类,所以若将父类实例想当然强行转换为某个子类,则很可能遭到类型不匹配的失败。此时为了确保万无一失,需要在类型转换之前增加条件判断,即利用instanceof检查该实例是否属于指定的子类类型,只有类型完全一致方可进行类型转换的操作。先做instanceof校验后做类型转换的代码例子如下所示:

	// 测试类型转换异常:原始数据与目标类型不匹配
private static void testConvertLyList() {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
// 在做强制类型转换之前,先把它的底细摸清楚。正所谓“知己知彼,百战不殆”
if (list instanceof ArrayList<?>) {
ArrayList<Integer> arrays = (ArrayList<Integer>) list;
System.out.println("arrays size=" + arrays.size());
} else {
System.out.println("arrays is not belong to ArrayList");
}
}

其它异常的预防措施大体类似,基本思路是检验某项操作的前提条件是否满足,所谓万变不离其宗,掌握了异常预防的要领即可举一反三。

因代码逻辑缺陷而导致的程序异常,可以通过改进相关业务逻辑加以预防,那么因系统资源不足而导致的程序错误,能否也采取类似的预防措施呢?前面在分析内存溢出和栈溢出错误的时候,两个示例代码都包含了方法递归的过程,这是否意味着,只要不进行递归调用,或者只进行有限次的递归调用,就能避免撑爆系统的错误吗?从表面上看,前述的内存溢出和栈溢出错误,都由无限次的递归调用而引发。然而这不能全赖递归,无限次递归只是产生错误的其中一种情况,事实上还有别的情况也会造成溢出错误,根源在于一个程序分配到的堆内存和栈内存大小是个有限的数值,倘若某项业务操作想要占据一块非常大的空间,或者某次方法调用需要传递一个非常大的参数,那么不必多次递归调用,只需仅仅一次调用就会让程序瘫痪了。
使用eclipse开发Java的话,默认的堆内存和栈内存大小在eclipse.ini里面设置,ini文件中的Xmx参数表示JVM最大的堆内存大小,该参数通常配置为512M;另一个Xss参数表示每个线程的栈内存大小,该参数默认为1M。所以,一旦程序意图占用超过512M的内存空间,就会报堆内存溢出的错误;一旦某次方法调用需要传送超过1M大小的参数信息,就会报栈溢出的错误。
举个实际应用的场景,比如在电脑上看电影,现在一个高清电影的视频大小普遍有好几个G,要是将几G大小的文件全加载进内存才播放,转眼间电脑内存便所剩无几。显然这种做法不可取,合理的方案是边加载边播放,每次只要提前加载当前播放位置后面若干秒的视频,同时释放掉已经播放完毕的那部分视频资源,那么真正用到的内存大小只有当前位置以及之后的一段视频缓冲,如此便能在极大程度上节约了内存资源消耗。
再来一个跟方法调用有关的场景,有时调用某方法需要传递图像数据,而位图格式的图像体积是相当大的。以一幅分辨率为800*600的图片为例,它共有800*600=48万个像素,每个像素又需要8位的灰度、8位的红色、8位的绿色、8位的蓝色加起来是4个字节空间,于是该图片的位图数据大小=48万*4字节=192万字节≈1.83M,那么光光这幅图片的数据便足以耗光程序的栈内存了。想想你去买房子,一套房子的总价要好几百万,难道要拎着数百捆的百元大钞去付房款吗?就算麻袋装得下这几百捆钞票,恐怕也没人拎得动无比沉重的麻袋。正常的做法是把钱存在银行里面,然后带上银行卡在售楼部直接刷卡,银行系统就知晓有多少资金发生了交易。同理,在方法调用时传递图片,无需直接传送完整的图像数据,而是先把图像保存为某个图片文件,再向该方法传递图片文件的存储路径,这样下级方法去指定路径读取图片便是。

更多Java技术文章参见《Java开发笔记(序)章节目录

上一篇:Java之异常机制(1) - 高效处理异常


下一篇:Java运行时异常和非运行时异常