Java开发笔记(七十三)常见的程序异常

一个程序开发出来之后,无论是用户还是程序员,都希望它稳定地运行,然而程序毕竟是人写的,人无完人哪能不犯点错误呢?就算事先考虑得天衣无缝,揣着一笔巨款跑去岛国买了栋抗震性能良好的海边别墅,谁料人算不如天算,碰到猴年马月遇上了一场大海啸,整个别墅被冲到山上去了。计算机程序也是如此,不管是人为的错误,还是意外的风险,都会导致程序在运行时异常退出。引起程序异常的原因多种多样,就已经介绍过的知识点而言,主要有这么几种可能发生异常的情况:数学运算异常、数组越界异常、字符串与日期格式异常、空指针异常、类型转换异常等等,接下来分别进行详细说明。

1、数学运算异常
最常见的算术异常当为除数为零,众所周知,在除法运算中除数是不能为零的,纵使数学家规定一除以零的结果等于无穷大,可是计算机该如何表达无穷大呢?要知道个人电脑的内存总共才几个G。既然有限的内存容纳不了无限的大小,想让程序计算一除以零就是不可能的事情了。接下来不妨通过一个除数为零的Java程序验证看看,测试用的方法代码示例如下:

	// 测试算术异常:除数为0
private static void testDivideByZero() {
int one = 1;
int zero = 0;
int result = one / zero;
System.out.println("divide result="+result);
}

运行以上的测试代码,果不其然观察到了异常日志“java.lang.ArithmeticException: / by zero”,可见除数为零是不正确的写法。
另一种算术异常也跟无限有关,像一除以三的结果为三分之一,使用小数表达的话便是0.33333333……这样的无限循环小数。当然由于浮点类型和双精度类型有精度限制,因此使用浮点数抑或双精度数存放三分之一,都只会精确到小数点后若干位,并不存在无限循环的问题。麻烦出在大小数BigDecimal上面,因为大小数默认是绝对精确的,只要开发者不指定大小数的精度位数,则系统会竭尽所能把大小数的精确值原原本本地表达出来。那么问题就来了,三分之一的数值乃无限循环小数,小数点后面的3有无限多个,似此无限的位数,依旧让有限的内存徒呼奈何。下面便是通过大小数计算一除以三的代码例子:

	// 测试算术异常:商是无限循环小数
private static void testDivideByDecimal() {
BigDecimal one = BigDecimal.valueOf(1);
BigDecimal three = BigDecimal.valueOf(3);
BigDecimal result = one.divide(three);
System.out.println("sqrt result="+result);
}

运行上面的除法代码,可见程序仍然打印了异常日志“java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.”,意思是无限小数没法用精确的十进制数来表达。

2、数组越界异常
假设某个数组只有三个元素,正常情况能够访问第一个、第二个和第三个元素,要是程序强行访问第四个元素,系统该怎么办?总不能无中生有变戏法变出一个吧,计算机程序可不是魔术师,它找不到第四个元素就崩溃退出了。比如以下的数组访问代码就重现了这个问题:

	// 测试越界异常:下标超出数组范围
private static void testArrayByIndex() {
int[] array = {1, 2, 3};
int item = array[3];
System.out.println("array item="+item);
}

运行以上的测试代码,程序果然输出了异常信息“java.lang.ArrayIndexOutOfBoundsException: 3”,此处的下标3代表数组的第四个元素,而该数组总共只有三个元素。
不光是数组存在越界异常,容器里的清单List也存在同样的问题,因为清单的索引类似数组的下标,一旦寻求访问的元素索引超出了清单大小,程序运行时也会扔出数组越界异常。用于演示通过索引访问清单元素的代码示例如下:

	// 测试越界异常:索引超出清单范围
private static void testListByIndex() {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
Integer item = list.get(5);
System.out.println("list item="+item);
}

运行上述的清单访问代码,依然可见程序扔出的异常描述“java.lang.ArrayIndexOutOfBoundsException: 5”,表示索引为5的位置已经超出了当前数组(其实是清单)的边界。

3、字符串与日期格式异常
调用String类的format方法进行字符串格式化之时,每种格式定义与数据类型是一一对应的,例如%d对应整型数,%s对应字符串,%b对应布尔值等等。所以格式化的参数值必须和它的格式要求相符,倘若二者匹配不了,这可如何是好?譬如原先定义的参数格式为%d,表示此处期望格式化一个整型数,结果后面的参数列表却传入某个字符串,难道字符串要格成整数?恐怕只能让程序嗝屁了。不信请看下面的字符串格式化代码:

	// 测试格式异常:字符串格式非法
private static void testStringByFormat() {
String str = String.format("%d", "Hello");
System.out.println("str="+str);
}

运行上面的格式化代码,毫无疑问程序无法正常运行,只能无奈地打印异常日志“java.util.IllegalFormatConversionException: d != java.lang.String”。
不单单字符串有格式要求,日期时间也有格式要求,如果需要把日期数据转换成字符串类型,就得在构造SimpleDateFormat实例时书写正确的时间格式,一个字不多一个字不少,倘若把分钟格式mm误写为mi,试试看程序会怎么运行以下的时间转换代码?

	// 测试格式异常:日期格式非法
private static void testDateByFormat() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mi:ss");
String strDate = sdf.format(new Date());
System.out.println("strDate="+strDate);
}

由于时间格式指定的分钟代号mi有误(正确的应为mm),因此运行以上的测试代码,程序也只能乖乖地打印出错信息“java.lang.IllegalArgumentException: Illegal pattern character 'I'”,表示mi里面的字母I是非法的格式字符。

4、空指针异常
面向对象的前提是有这个对象,好比这个春节你妈喊你带上对象回家过年,可要是对象连影都见不着,你妈给你对象准备的嘘寒问暖就都泡汤了。在Java代码里面,除了少数几个基本类型,其余绝大多数类型必须先给对象创建实例,然后才能访问该对象的各项成员属性和成员方法。假如不给对象分配实例,就想牵起对象的小手,系统会果断地告诉你:门都没有!譬如常用的字符串类型,不管是new出一个字符串实例,还是硬塞给它一个双引号括起来的具体串,都算作分配了对象实例。如果声明字符串对象时啥都不干,或者随便填了个null,那真是对不起了,程序认为该对象没有初始化,就不会给它分配存储空间。后面的代码再想操作这个对象的时候,找不到对象地址只能报空指针异常了,有关的异常重现代码如下所示:

	// 测试空指针异常:对象不存在
private static void testStringByNull() {
String str = null;
int length = str.length();
System.out.println("str length="+length);
}

运行如上的测试代码,观察到打印的异常信息为“java.lang.NullPointerException”,显然被系统揪到了偷懒的小辫子。

5、类型转换异常
在运用多态技术的时候,常常将某个父类实例转换成子类的类型,以便调用子类自身的方法。但这得确保原来的父类实例来自于该子类才行,倘使父类实例来自另一个子类B,代码却想把它强行转换为子类A,也就是俗称的张冠李戴,系统自然不允许这种胡搅蛮缠的情况。尽管开发者一般不会糊涂,可是难保偶尔脑袋抽筋,比如数组工具Arrays的asList方法返回一个清单对象,乍看过去与列表类型ArrayList是一个家伙,谁知真要转换类型的话,程序居然会不认账!这里转换清单类型的代码示例如下:

	// 测试类型转换异常:原始数据与目标类型不匹配
private static void testConvertLyList() {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
ArrayList<Integer> arrays = (ArrayList<Integer>) list;
System.out.println("arrays size="+arrays.size());
}

运行上述的类型转换代码,结果输出异常日志“java.lang.ClassCastException: java.util.Arrays$ArrayList cannot be cast to java.util.ArrayList”,没想到此列表非彼列表,当真是大意不得。

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

上一篇:Read4096


下一篇:Java基础知识强化之IO流笔记01:异常的概述和分类