第57条-只针对异常的情况才使用异常

第57条、只针对异常的情况才使用异常

不知道你否则遇见过下面的代码。

1
2
3
4
5
6
try {
int i = 0;3
while (true)
range[i++].climb();
}catch (ArrayIndexOutOfBoundsException e) {
}

这段代码的意图不是很明显,其本意就是遍历变量range[]中的每一个元素,并执行元素的climb(),当下标超出range[]的长度时,将会直接抛出ArrayIndexOutOfBoundsException异常,catch代码块将会捕获到该异常,但是未作任何处理,只是将该错误视为正常工作流程的一部分来看待。这样的写法确实给人一种匪夷所思的感觉,让我们再来看一下修改后的写法。

1
2
3
for (Mountain m : range) {
m.climb();
}

和之前的写法相比其可读性不言而喻。那么为什么又有人会用第一种写法呢?显然他们是被误导了,他们企图避免for-each循环中JVM对每次数组访问都要进行的越界检查。这无疑是多余的,甚至适得其反,因为将代码放在try-catch块中反而阻止了JVM的某些特定优化,至于数组的边界检查,现在很多JVM实现都会将他们优化掉了。在实际的测试中,我们会发现采用异常的方式其运行效率要比正常的方式慢很多。
除了刚刚提到的效率和代码可读性问题,第一种写法还会掩盖一些潜在的Bug,假设数组元素的climb()中也会访问某一数组,并且在访问的过程中出现了数组越界的问题,基于该错误,JVM将会抛出ArrayIndexOutOfBoundsException异常,不幸的是,该异常将会被climb()之外catch语句捕获,在未做任何处理之后,就按照正常流程继续执行了,这样Bug也就此被隐藏起来。
这个例子的教训很简单:”异常应该只用于异常的情况下,它们永远不应该用于正常的控制流”。虽然有的时候有人会说这种怪异的写法可以带来性能上的提升,即便如此,随着平台实现的不断改进,这种异常模式的性能优势也不可能一直保持。然而,这种过度聪明的模式带来的微妙的Bug,以及维护的痛苦却依然存在。
根据这条原则,我们在设计API的时候也是会有所启发的。设计良好的API不应该为了正常的控制流而使用异常。如Iterator,JDK在设计时充分考虑到这一点,客户端在执行next()之前,需要先调用hasNext()已确认是否还有可读的集合元素,见如下代码。

1
2
3
for (Iterator i = collection.iterator(); i.hasNext(); ) {
Foo f = i.next();
}

如果Iterator缺少hasNext(),改为下面的写法。

1
2
3
4
5
6
try {
Iterator i = collection.iterator();
while (true)
Foo f = i.next();
}catch (NoSuchElementException e) {
}

这应该非常类似于本条目开始时给出的遍历数组的例子。在实际的设计中,还有另外一种方式,即验证可识别的错误返回值,然而该方式并不适合于此例,因为对于next(),返回null可能是合法的。那么这两种设计方式在实际应用中有哪些区别呢?

  1. 如果是缺少同步的并发访问,或者可被外界改变状态,使用可识别返回值的方法是非常必要的,因为在测试状态(hasNext())和对应的调用(next())之间存在一个时间窗口,在该窗口中,对象可能会发生状态的变化。因此,在该种情况下应选择返回可识别的错误返回值的方式。
  2. 如果状态测试方法(hasNext())和相应的调用方法(next())使用的是相同的代码,出于性能上的考虑,没有必要重复两次相同的工作,此时应该选择返回可识别的错误返回值的方式。
  3. 对于其他情形则应该尽可能考虑”状态测试”的设计方式,因为它可以带来更好的可读性。

    总结

    需要异常的地方才使用异常,不能使用异常去控制正常的业务流程。

第45条-将局部变量的作用域最小化

优点

增强代码的可读性,可维护性,并降低错误的可能性。

优化思路

  1. 要使局部变量的作用域最小化,最有力的方法就是在第一次使用它的地方声明,这样有便于读者集中注意力,不容易分散;如果在之前申明,阅读者很容易忘记变量的类型和变量的意义。
  2. 过早的声明变量,使得变量的作用域过早的扩展(作用域扩大),变量销毁的时间过于晚。
  3. 变量的作用域从申明点开始,到这个方法的结束。
  4. 局部变量声明的时候,都应该要先初始化变量。比如,A a;写成A a=null;相对更好,更直观的看到a = null
  5. 循环的使用,forwhileforwhile好用。
    for变量定义在for的作用域内部,while则在外部。这样容易造成变量使用错误,而不会报异常。
    for更简短,增强可读性。
  6. 将局部变量的作用域最小化。如果一个方法有2个操作(不只是业务),可以拆分成2个方法,变量不会冲突,耦合度低。(平时开发也是这样,耦合度低,扩展性好,可读性强,好维护)

第14章-类型的信息RTTI

以JDK6为例

介绍

RTTI运行时类型信息使得可以在程序运行时发现和使用类型信息。

Run-Time Type Information

使用方式

Java是如何在运行时识别对象和类的信息的,主要有两种方式(还有辅助的第三种方式,见下描述)

  1. “传统的”RTTI,它假定在编译时已经知道所有的类型,比如Shape s = (Shape)s1
  2. “反射”机制,它运行在运行时发现和使用类的信息,即使用Class.forName()
  3. 关键字instanceof,它返回一个bool值,它保持类型的概念,它指的是”你是这个类吗?或者你是这个类的派生类吗?”。而如果用==equals比较实际的Class对象,就没有考虑继承它或者是这个确切的类型,或者不是。

    工作原理

    RTTI主要用来运行时获取对象到底是什么具体的类型。
    RTTI运行的时候,识别一个对象的类型。(运行的时候获取对象确切的类型)
    要理解RTTI在Java中的工作原理,首先必须知道类型信息在运行时是如何表示的,这项工作是由称为Class对象的特殊对象完成的,它包含与类有关的信息。Java Class对象来执行其RTTI,使用类加载器的子系统实现。
    类是程序的重要组成部分,每个类都有一个Class对象,每当编写并编译一个新类就会产生一个Class对象,它被保存在一个同名的.class文件中。在运行时,生成这个类的对象时,运行这个程序的Java虚拟机(JVM)会确认这个类的Class对象是否已经加载,如果尚未加载,JVM就会根据类名查找.class文件,并将其载入,一旦这个类的Class对象被载入内存,它就被用来创建这个类的所有对象。
    在运行时使用类型信息,就必须首先获得对恰当的Class对象的引用,获取方式有三种。

    第1种

    如果没有持有该类型的对象,则Class.forName()就是实现此功能的便捷途,因为它不需要对象信息。

    第2种

    如果已经拥有类型的对象,那就可以通过调用getClass()方法来获取Class引用,它将返回表示该对象的实际类型的Class引用。
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×