每个程序员都希望自己的程序稳定运行,不要隔三岔五出什么差错,可是程序运行时冒出来的各种异常着实烦人,令人不胜其扰。虽然可以在代码中补上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开发笔记(十六)非此即彼的条件分支

    前面花了大量篇幅介绍布尔类型及相应的关系运算和逻辑运算,那可不仅仅是为了求真值或假值,更是为了通过布尔值控制流程的走向.在现实生活中,常常需要在岔路口抉择走去何方,往南还是往北,向东还是向西?在Jav ...

  2. Java开发笔记(九十六)线程的基本用法

    每启动一个程序,操作系统的内存中通常会驻留该程序的一个进程,进程包含了程序的完整代码逻辑.一旦程序退出,进程也就随之结束:反之,一旦强行结束进程,程序也会跟着退出.普通的程序代码是从上往下执行的,遇到 ...

  3. Java开发学习(三十六)----SpringBoot三种配置文件解析

    一. 配置文件格式 我们现在启动服务器默认的端口号是 8080,访问路径可以书写为 http://localhost:8080/books/1 在线上环境我们还是希望将端口号改为 80,这样在访问的时 ...

  4. Java开发学习(二十六)----SpringMVC返回响应结果

    SpringMVC接收到请求和数据后,进行了一些处理,当然这个处理可以是转发给Service,Service层再调用Dao层完成的,不管怎样,处理完以后,都需要将结果告知给用户. 比如:根据用户ID查 ...

  5. Java学习笔记(十六)——Java RMI

    [前面的话] 最近过的好舒服,每天过的感觉很充实,一些生活和工作的技巧注意了就会发现,其实生活也是可以过的如此的有滋有味,满足现在的状况,并且感觉很幸福. 学习java RMI的原因是最近在使用dub ...

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

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

  7. 【Java学习笔记之十六】浅谈Java中的继承与多态

    1.  什么是继承,继承的特点? 子类继承父类的特征和行为,使得子类具有父类的各种属性和方法.或子类从父类继承方法,使得子类具有父类相同的行为. 特点:在继承关系中,父类更通用.子类更具体.父类具有更 ...

  8. Java开发笔记(十)一元运算符的技巧

    前面讲到赋值运算符的时候,提到“x = x+7”可以被“x += 7”所取代,当然Java编程中给某个变量自加7并不常见,常见的是给某变量自加1,就像走台阶,一般都是一级一级台阶地走,犯不着一下子跳上 ...

  9. Java开发笔记(十二)布尔变量论道与或非

    在编程语言的设计之初,它们除了可以进行数学计算,还常常用于逻辑推理和条件判断.为了实现逻辑判断的功能,Java引入了一种布尔类型boolean,用来表示“真”和“假”.该类型的变量只允许两个取值,即t ...

  10. Java开发笔记(十四)几种运算符的优先级顺序

    到目前为止,我们已经学习了Java语言的好几种运算符,包括算术运算符.赋值运算符.逻辑运算符.关系运算符等基础运算符,并且在书写赋值语句时都没添加圆括号,显然是默认了先完成算术.逻辑.关系等运算,最后 ...

随机推荐

  1. 【原创】XAF Winfrom 支持4K分辨率

    Program.cs 添加以下代码 [STAThread] static void Main() { //支持4K显示 DevExpress.XtraEditors.WindowsFormsSetti ...

  2. 购物车自己sql错误

    $user_id=$_GET['user_id']; if(!$user_id){ $arr=array('code'=>-1,'data'=>"用户不存在"); ec ...

  3. BUAA_OO Summary——多项式求导问题

    从C.DS.计组一路折磨过来, 几乎都在采用过程化.函数式的编程思想.初接触面向对象的项目开发,经过了三周的对多项式求导问题的迭代开发,经历了设计.coding.测评环节,算是对面向对象有了一定的认识 ...

  4. db2数据库常见问题

    db2数据库不能轻易改变表结构,不然表会进入暂挂状态,造成表被锁住. 解锁表语句:call sysproc.admin_cmd('reorg table <table name>');

  5. MySQL乐观锁为什么可以防止并发

    问题引入 本文介绍的是最常用的也是mysql默认的innoDB引擎 Read committed隔离级别下事物的并发.这种情况下的事物特点是 读:在一个事物里面的select语句 不会受到其他事物(不 ...

  6. 【从零开始搭建自己的.NET Core Api框架】(五)由浅入深详解CORS跨域机制并快速实现

    系列目录 一.  创建项目并集成swagger 1.1 创建 1.2 完善 二. 搭建项目整体架构 三. 集成轻量级ORM框架——SqlSugar 3.1 搭建环境 3.2 实战篇:利用SqlSuga ...

  7. ActiveMQ嵌入Tomcat

    在一些项目中,单独开启一个ActiveMQ,对于项目实施来说有时略显繁琐.所以我们将ActiveMQ内嵌到Tomcat,Tomcat启动同时就顺带启动了ActiveMQ.由此我们需要掌握三个个重要的知 ...

  8. 互联网最新kafka技术面试题含答案

    1.Kafka 的设计时什么样的呢? Kafka 将消息以 topic 为单位进行归纳 将向 Kafka topic 发布消息的程序成为 producers. 将预订 topics 并消费消息的程序成 ...

  9. Spring高级装配bean

    目录 spring profile 条件化的bean声明 自动装配与歧义性 bean的作用域 Spring表达式语言 一.环境与profile 配置profile  bean 在软件开发的时候,有一个 ...

  10. cassandra vs mongo (1)存储引擎

    摘要 在MongoDB 初识篇中谈到过Mongo 与 Cassandra的区别,这边再谈谈Mongo与Cassandra的存储引擎差别 概括 存储引擎: 类型 功能 应用 hash 增删改.随机读.顺 ...