Java异常架构

Java异常简介

  Java异常是Java提供的一种识别及响应错误的一致性机制。
  Java异常机制可使程序中异常处理代码和正常业务代码分离,保证程序代码更加优雅,并提升程序健壮性。在有效使用异常的状况下,异常能清晰的回答what, where, why这3个问题:异常类型回答了“什么”被抛出,异常堆栈跟踪回答了“在哪“抛出,异常信息回答了“为何“会抛出。java

Java异常机制用到的几个关键字:try、catch、finally、throw、throws。
try        -- 用于监听。将要被监听的代码(可能抛出异常的代码)放在try语句块以内,当try语句块内发生异常时,异常就被抛出。
catch   -- 用于捕获异常。catch用来捕获try语句块中发生的异常。
finally  -- finally语句块老是会被执行。它主要用于回收在try块里打开的物力资源(如数据库链接、网络链接和磁盘文件)。只有finally块,执行完成之 后,才会回来执行try或者catch块中的return或者throw语句,若是finally中使用了return或者throw等终止方法的语句, 则就不会跳回执行,直接中止。
throw   -- 用于抛出异常。
throws -- 用在方法签名中,用于声明该方法可能抛出的异常。程序员

下面经过几个示例对这几个关键字进行简单了解。数据库

示例一: 了解try和catch基本用法数组

public class Demo1 {

    public static void main(String[] args) {
        try {
            int i = 10/0;
              System.out.println("i="+i); 
        } catch (ArithmeticException e) {
              System.out.println("Caught Exception"); 
            System.out.println("e.getMessage(): " + e.getMessage()); 
            System.out.println("e.toString(): " + e.toString()); 
            System.out.println("e.printStackTrace():");
            e.printStackTrace(); 
        }
    }
}

运行结果安全

Caught Exception
e.getMessage(): / by zero
e.toString(): java.lang.ArithmeticException: / by zero
e.printStackTrace():
java.lang.ArithmeticException: / by zero
    at Demo1.main(Demo1.java:6)

结果说明在try语句块中有除数为0的操做,该操做会抛出java.lang.ArithmeticException异常。经过catch,对该异常进行捕获。
观察结果咱们发现,并无执行System.out.println("i="+i)。这说明try语句块发生异常以后,try语句块中的剩余内容就不会再被执行了。 网络

示例二: 了解finally的基本用法架构

在"示例一"的基础上,咱们添加finally语句。并发

public class Demo2 {

    public static void main(String[] args) {
        try {
            int i = 10/0;
              System.out.println("i="+i); 
        } catch (ArithmeticException e) {
              System.out.println("Caught Exception"); 
            System.out.println("e.getMessage(): " + e.getMessage()); 
            System.out.println("e.toString(): " + e.toString()); 
            System.out.println("e.printStackTrace():");
            e.printStackTrace(); 
        } finally {
            System.out.println("run finally");
        }
    }
}

运行结果app

Caught Exception
e.getMessage(): / by zero
e.toString(): java.lang.ArithmeticException: / by zero
e.printStackTrace():
java.lang.ArithmeticException: / by zero
    at Demo2.main(Demo2.java:6)
run finally

结果说明最终执行了finally语句块。 框架

示例三: 了解throws和throw的基本用法

throws是用于声明抛出的异常,而throw是用于抛出异常。

class MyException extends Exception {
    public MyException() {}
    public MyException(String msg) {
        super(msg);
    }
}

public class Demo3 {

    public static void main(String[] args) {
        try {
            test();
        } catch (MyException e) {
            System.out.println("Catch My Exception");
            e.printStackTrace();
        }
    }
    public static void test() throws MyException{
        try {
            int i = 10/0;
              System.out.println("i="+i); 
        } catch (ArithmeticException e) {
            throw new MyException("This is MyException"); 
        }
    }
}

运行结果

Catch My Exception
MyException: This is MyException
    at Demo3.test(Demo3.java:24)
    at Demo3.main(Demo3.java:13)

结果说明
   MyException是继承于Exception的子类。test()的try语句块中产生ArithmeticException异常(除数为 0),并在catch中捕获该异常;接着抛出MyException异常。main()方法对test()中抛出的MyException进行捕获处理。

Java异常框架

Java异常架构图


1. Throwable
  Throwable是 Java 语言中全部错误或异常的超类。
  Throwable包含两个子类: ErrorException。它们一般用于指示发生了异常状况。
  Throwable包含了其线程建立时线程执行堆栈的快照,它提供了printStackTrace()等接口用于获取堆栈跟踪数据等信息。

2. Exception
  Exception及其子类是 Throwable 的一种形式,它指出了合理的应用程序想要捕获的条件。

3. RuntimeException
  RuntimeException是那些可能在 Java 虚拟机正常运行期间抛出的异常的超类。
  编译器不会检查RuntimeException异常。例 如,除数为零时,抛出ArithmeticException异常。RuntimeException是ArithmeticException的超类。 当代码发生除数为零的状况时,假若既"没有经过throws声明抛出ArithmeticException异常",也"没有经过 try...catch...处理该异常",也能经过编译。这就是咱们所说的"编译器不会检查RuntimeException异常"!
  若是代码会产生RuntimeException异常,则须要经过修改代码进行避免。例如,若会发生除数为零的状况,则须要经过代码避免该状况的发生!

4. Error
  和Exception同样,Error也是Throwable的子类。它用于指示合理的应用程序不该该试图捕获的严重问题,大多数这样的错误都是异常条件。
  和RuntimeException同样,编译器也不会检查Error。

Java将可抛出(Throwable)的结构分为三种类型:被检查的异常(Checked Exception),运行时异常(RuntimeException)和错误(Error)。

(01) 运行时异常
定义: RuntimeException及其子类都被称为运行时异常。
特色: Java编译器不会检查它。也 就是说,当程序中可能出现这类异常时,假若既"没有经过throws声明抛出它",也"没有用try-catch语句捕获它",仍是会编译经过。例如,除 数为零时产生的ArithmeticException异常,数组越界时产生的IndexOutOfBoundsException异常,fail- fail机制产生的ConcurrentModificationException异常等,都属于运行时异常。
  虽然Java编译器不会检查运行时异常,可是咱们也能够经过throws进行声明抛出,也能够经过try-catch对它进行捕获处理。
  若是产生运行时异常,则须要经过修改代码来进行避免。例如,若会发生除数为零的状况,则须要经过代码避免该状况的发生!

(02) 被检查的异常
定义Exception类自己,以及Exception的子类中除了"运行时异常"以外的其它子类都属于被检查异常。
特色: Java编译器会检查它。此 类异常,要么经过throws进行声明抛出,要么经过try-catch进行捕获处理,不然不能经过编译。例 如,CloneNotSupportedException就属于被检查异常。当经过clone()接口去克隆一个对象,而该对象对应的类没有实现 Cloneable接口,就会抛出CloneNotSupportedException异常。
  被检查异常一般都是能够恢复的。

(03) 错误
定义: Error类及其子类。
特色: 和运行时异常同样,编译器也不会对错误进行检查。
  当资源不足、约束失败、或是其它程序没法继续运行的条件发生时,就产生错误。程序自己没法修复这些错误的。例如,VirtualMachineError就属于错误。
  按照Java惯例,咱们是不该该是实现任何新的Error子类的!

对于上面的3种结构,咱们在抛出异常或错误时,到底该哪种?《Effective Java》中给出的建议是:对于能够恢复的条件使用被检查异常,对于程序错误使用运行时异常。

Effective Java 》中关于异常处理的几条建议:

第1条: 只针对不正常的状况才使用异常

建议:异常只应该被用于不正常的条件,它们永远不该该被用于正常的控制流。
经过比较下面的两份代码进行说明。
代码1

try {
    int i=0;
    while (true) {
        arr[i]=0;
        i++;
    }
} catch (IndexOutOfBoundsException e) { }

代码2

for (int i=0; i<arr.length; i++) {
    arr[i]=0;
}

  两份代码的做用都是遍历arr数组,并设置数组中每个元素的值为0。代码1的是经过异常来终止,看起来很是难懂,代码2是经过数组边界来终止。咱们应该避免使用代码1这种方式,主要缘由有三点:
• 异常机制的设计初衷是用于不正常的状况,因此不多会会JVM实现试图对它们的性能进行优化。因此,建立、抛出和捕获异常的开销是很昂贵的。
• 把代码放在try-catch中返回阻止了JVM实现原本可能要执行的某些特定的优化。
• 对数组进行遍历的标准模式并不会致使冗余的检查,有些现代的JVM实现会将它们优化掉。

实际上,基于异常的模式比标准模式要慢得多。测试代码以下:

public class Advice1 {

    private static int[] arr = new int[]{1,2,3,4,5};
    private static int SIZE = 10000;

    public static void main(String[] args) {

        long s1 = System.currentTimeMillis();
        for (int i=0; i<SIZE; i++)
            endByRange(arr);
        long e1 = System.currentTimeMillis();
        System.out.println("endByRange time:"+(e1-s1)+"ms" );

        long s2 = System.currentTimeMillis();
        for (int i=0; i<SIZE; i++)
            endByException(arr);
        long e2 = System.currentTimeMillis();
        System.out.println("endByException time:"+(e2-s2)+"ms" );
    }

    // 遍历arr数组: 经过异常的方式
    private static void endByException(int[] arr) {
        try {
            int i=0;
            while (true) {
                arr[i]=0;
                i++;
                //System.out.println("endByRange: arr["+i+"]="+arr[i]);
            }
        } catch (IndexOutOfBoundsException e) {
        }
    }

    // 遍历arr数组: 经过边界的方式
    private static void endByRange(int[] arr) {
        for (int i=0; i<arr.length; i++) {
            arr[i]=0;
            //System.out.println("endByException: arr["+i+"]="+arr[i]);
        }
    }
}

运行结果

endByRange time:8ms
endByException time:16ms

结果说明:经过异常遍历的速度比普通方式遍历数组慢不少!

第2条: 对于可恢复的条件使用被检查的异常,对于程序错误使用运行时异常

运行时异常     -- RuntimeException类及其子类都被称为运行时异常。
被检查的异常 -- Exception类自己,以及Exception的子类中除了"运行时异常"以外的其它子类都属于被检查异常。

  它们的区别是:Java编译器会对"被检查的异常"进行检查,而对"运行时异常"不会检查。也 就是说,对于被检查的异常,要么经过throws进行声明抛出,要么经过try-catch进行捕获处理,不然不能经过编译。而对于运行时异常,假若既" 没有经过throws声明抛出它",也"没有用try-catch语句捕获它",仍是会编译经过。固然,虽然说Java编译器不会检查运行时异常,可是,我 们一样能够经过throws对该异常进行说明,或经过try-catch进行捕获。
  ArithmeticException(例如,除数为0),IndexOutOfBoundsException(例如,数组越界)等都属于运行时 异常。对于这种异常,咱们应该经过修改代码进行避免它的产生。而对于被检查的异常,则能够经过处理让程序恢复运行。例如,假设由于一个用户没有存储足够数 量的前,因此他在企图在一个收费电话上进行呼叫就会失败;因而就将一个被检查异常抛出。

第3条: 避免没必要要的使用被检查的异常

  "被检查的异常"是Java语言的一个很好的特性。与返回代码不一样,"被检查的异常"会强迫程序员处理例外的条件,大大提升了程序的可靠性。
  可是,过度使用被检查异常会使API用起来很是不方便。若是一个方法抛出一个或多个被检查的异常,那么调用该方法的代码则必须在一个或多个catch 语句块中处理这些异常,或者必须经过throws声明抛出这些异常。 不管是经过catch处理,仍是经过throws声明抛出,都给程序员添加了不可忽略的负担。

  适用于"被检查的异常"必须同时知足两个条件:第一,即便正确使用API并不能阻止异常条件的发生。第二,一旦产生了异常,使用API的程序员能够采起有用的动做对程序进行处理。

第4条: 尽可能使用标准的异常

代码重用是值得提倡的,这是一条通用规则,异常也不例外。重用现有的异常有几个好处:
第一,它使得你的API更加易于学习和使用,由于它与程序员原来已经熟悉的习惯用法是一致的。
第二,对于用到这些API的程序而言,它们的可读性更好,由于它们不会充斥着程序员不熟悉的异常。
第三,异常类越少,意味着内存占用越小,而且转载这些类的时间开销也越小。

Java标准异常中有几个是常常被使用的异常。以下表格:
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ 异常                                                                  ┃ 使用场合                                                              ┃
┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
┃ IllegalArgumentException                             │ 参数的值不合适                                                    ┃
┠───────────────────────────┼─────────────────────────────┨
┃ IllegalStateException                                     │ 参数的状态不合适                                                 ┃
┠───────────────────────────┼─────────────────────────────┨
┃ NullPointerException                                      │ 在null被禁止的状况下参数值为null                     ┃
┠───────────────────────────┼─────────────────────────────┨
┃ IndexOutOfBoundsException                       │ 下标越界                                                               ┃
┠───────────────────────────┼─────────────────────────────┨
┃ ConcurrentModificationException                │ 在禁止并发修改的状况下,对象检测到并发修改 ┃
┠───────────────────────────┼──────────── ─────────────────┨
┃ UnsupportedOperationException                │ 对象不支持客户请求的方法                                   ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
  虽然它们是Java平台库迄今为止最常被重用的异常,可是,在许可的条件下,其它的异常也能够被重用。例如,若是你要实现诸如复数或者矩阵之类的算术对象,那么重用ArithmeticException和NumberFormatException将是很是合适的。若是一个异常知足你的须要,则不要犹豫,使用就能够,不过你必定要确保抛出异常的条件与该异常的文档中描述的条件一致。这种重用必须创建在语义的基础上,而不是名字的基础上!
  最后,必定要清楚,选择重用哪种异常并无必须遵循的规则。例 如,考虑纸牌对象的情形,假设有一个用于发牌操做的方法,它的参数(handSize)是发一手牌的纸牌张数。假设调用者在这个参数中传递的值大于整副牌 的剩余张数。那么这种情形既能够被解释为IllegalArgumentException(handSize的值太大),也能够被解释为 IllegalStateException(相对客户的请求而言,纸牌对象的纸牌太少)。

第5条: 抛出的异常要适合于相应的抽象

  若是一个方法抛出的异常与它执行的任务没有明显的关联关系,这种情形会让人不知所措。当一个方法传递一个由低层抽象抛出的异常时,每每会发生这种状况。这种状况发生时,不只让人困惑,并且也"污染"了高层API。
  为了不这个问题,高层实现应该捕获低层的异常,同时抛出一个能够按照高层抽象进行介绍的异常。这种作法被称为"异常转译(exception translation)"。

  例如,在Java的集合框架AbstractSequentialList的get()方法以下(基于JDK1.7.0_40):

public E get(int index) {
    try {
        return listIterator(index).next();
    } catch (NoSuchElementException exc) {
        throw new IndexOutOfBoundsException("Index: "+index);
    }
}

  listIterator(index)会 返回ListIterator对象,调用该对象的next()方法可能会抛出NoSuchElementException异常。而在get()方法中, 抛出NoSuchElementException异常会让人感到困惑。因此,get()对NoSuchElementException进行了捕获,并 抛出了IndexOutOfBoundsException异常。即,至关于将NoSuchElementException转译成了 IndexOutOfBoundsException异常。

第6条: 每一个方法抛出的异常都要有文档

  要单独的声明被检查的异常,而且利用Javadoc的@throws标记,准确地记录下每一个异常被抛出的条件。
  若是一个类中的许多方法处于一样的缘由而抛出同一个异常,那么在该类的文档注释中对这个异常作文档,而不是为每一个方法单独作文档,这是能够接受的。

第7条: 在细节消息中包含失败 -- 捕获消息

  简而言之,当咱们自定义异常或者抛出异常时,应该包含失败相关的信息。
  当一个程序因为一个未被捕获的异常而失败的时候,系统会自动打印出该异常的栈轨迹。在栈轨迹中包含该异常的字符串表示。典型状况下它包含该异常类的类名,以及紧随其后的细节消息。

第8条: 努力使失败保持原子性

  当一个对象抛出一个异常以后,咱们总指望这个对象仍然保持在一种定义良好的可用状态之中。对于被检查的异常而言,这尤其重要,由于调用者一般指望从被检查的异常中恢复过来。
  通常而言,一个失败的方法调用应该保持使对象保持在"它在被调用以前的状态"。具备这种属性的方法被称为具备"失败原子性(failure atomic)"。能够理解为,失败了还保持着原子性。对象保持"失败原子性"的方式有几种:

(01) 设计一个非可变对象。
(02) 对于在可变对象上执行操做的方法,得到"失败原子性"的最多见方法是,在执行操做以前检查参数的有效性。以下(Stack.java中的pop方法):

public Object pop() {
    if (size==0)
        throw new EmptyStackException();
    Object result = elements[--size];
    elements[size] = null;
    return result;
}

(03) 与上一种方法相似,能够对计算处理过程调整顺序,使得任何可能会失败的计算部分都发生在对象状态被修改以前。
(04) 编写一段恢复代码,由它来解释操做过程当中发生的失败,以及使对象回滚到操做开始以前的状态上。
(05) 在对象的一份临时拷贝上执行操做,当操做完成以后再把临时拷贝中的结果复制给原来的对象。
虽然"保持对象的失败原子性"是指望目标,但它并不老是能够作获得。例如,若是多个线程企图在没有适当的同步机制的状况下,并发的访问一个对象,那么该对象就有可能被留在不一致的状态中。

  即便在能够实现"失败原子性"的场合,它也不是总被指望的。对于某些操做,它会显著的增长开销或者复杂性。
  总的规则是:做为方法规范的一部分,任何一个异常都不该该改变对象调用该方法以前的状态,若是这条规则被违反,则API文档中应该清楚的指明对象将会处于什么样的状态。

第9条: 不要忽略异常

  当一个API的设计者声明一个方法会抛出某个异常的时候,他们正在试图说明某些事情。因此,请不要忽略它!忽略异常的代码以下:

try {
    ...
} catch (SomeException e) {
}

  空的catch块会使异常达不到应有的目的,异常的目的是强迫你处理不正常的条件。忽略一个异常,就如同忽略一个火警信号同样 -- 若把火警信号器关闭了,那么当真正的火灾发生时,就没有人看到火警信号了。因此,至少catch块应该包含一条说明,用来解释为何忽略这个异常是合适 的。

《Java Puzzles》中关于异常的几个谜题:

谜题1: 优柔寡断

看看下面的程序,它到底打印什么?

public class Indecisive {

    public static void main(String[] args) {
        System.out.println(decision());
    }

    private static boolean decision() {
        try {
            return true;
        } finally {
            return false;
        }
    }
}

运行结果

false

结果说明
  在一个 try-finally 语句中,finally 语句块老是在控制权离开 try 语句块时执行的。不管 try 语句块是正常结束的,仍是意外结束的, 状况都是如此。
  一条语句或一个语句块在它抛出了一个异常,或者对某个封闭型语句执行了一个 break 或 continue,或是象这个程序同样在方法中执行了一个return 时,将发生意外结束。它们之因此被称为意外结束,是由于它们阻止程序去按顺序执行下面的语句。当 try 语句块和 finally 语句块都意外结束时, try 语句块中引起意外结束的缘由将被丢弃, 而整个 try-finally 语句意外结束的缘由将于 finally 语句块意外结束的缘由相同。在这个程序中,在 try 语句块中的 return 语句所引起的意外结束将被丢弃, try-finally 语句意外结束是由 finally 语句块中的 return 而形成的。

  简单地讲, 程序尝试着 (try) (return) 返回 true, 可是它最终 (finally) 返回(return)的是 false。丢弃意外结束的缘由几乎永远都不是你想要的行为, 由于意外结束的最初缘由可能对程序的行为来讲会显得更重要。对于那些在 try 语句块中执行 break、continue 或 return 语句,只是为了使其行为被 finally 语句块所否决掉的程序,要理解其行为是特别困难的。总之,每个 finally 语句块都应该正常结束,除非抛出的是不受检查的异常。 千万不要用一个 return、break、continue 或 throw 来退出一个 finally 语句块,而且千万不要容许将一个受检查的异常传播到一个 finally 语句块以外去。对于语言设计者, 也许应该要求 finally 语句块在未出现不受检查的异常时必须正常结束。朝着这个目标,try-finally 结构将要求 finally 语句块能够正常结束。return、break 或 continue 语句把控制权传递到 finally 语句块以外应该是被禁止的, 任何能够引起将被检查异常传播到 finally 语句块以外的语句也一样应该是被禁止的。

谜题2: 极端难以想象

下面的三个程序每个都会打印些什么? 不要假设它们均可以经过编译。
第一个程序

import java.io.IOException;

public class Arcane1 {

    public static void main(String[] args) {
        try {
            System.out.println("Hello world");
        } catch(IOException e) {
            System.out.println("I've never seen println fail!");
        }
    }
}

第二个程序

public class Arcane2 {
    public static void main(String[] args) {
        try {
            // If you have nothing nice to say, say nothing
        } catch(Exception e) {
            System.out.println("This can't happen");
        }
    }
}

第三个程序

interface Type1 {
    void f() throws CloneNotSupportedException;
}

interface Type2 {
    void f() throws InterruptedException;
}

interface Type3 extends Type1, Type2 {
}

public class Arcane3 implements Type3 {
    public void f() {
        System.out.println("Hello world");
    }
    public static void main(String[] args) {
        Type3 t3 = new Arcane3();
        t3.f();
    }
}

运行结果
(01) 第一个程序编译出错!

Arcane1.java:9: exception java.io.IOException is never thrown in body of corresponding try statement
        } catch(IOException e) {
          ^
1 error

(02) 第二个程序能正常编译和运行。
(03) 第三个程序能正常编译和运行。输出结果是: Hello world

结果说明
(01) Arcane1展现了被检查异常的一个基本原则。它看起来应该是能够编译的:try 子句执行 I/O,而且 catch 子句捕获 IOException 异常。可是这个程序不能编译,由于 println 方法没有声明会抛出任何被检查异常,而IOException 却正是一个被检查异常。语言规范中描述道:若是一个 catch 子句要捕获一个类型为 E 的被检查异常, 而其相对应的 try 子句不能抛出 E 的某种子类型的异常,那么这就是一个编译期错误。

(02) 基于一样的理由,第二个程序,Arcane2,看起来应该是不能够编译的,可是它却能够。它之因此能够编译,是由于它惟一的 catch 子句检查了 Exception。尽管在这一点上十分含混不清,可是捕获 Exception 或 Throwble 的 catch 子句是合法的,无论与其相对应的 try 子句的内容为什么。尽管 Arcane2 是一个合法的程序,可是 catch 子句的内容永远的不会被执行,这个程序什么都不会打印。

(03) 第三个程序,Arcane3,看起来它也不能编译。方法 f 在 Type1 接口中声明要抛出被检查异常 CloneNotSupportedException,而且在 Type2 接口中声明要抛出被检查异常 InterruptedException。Type3 接口继承了 Type1 和 Type2,所以, 看起来在静态类型为 Type3 的对象上调用方法 f 时, 有潜在可能会抛出这些异常。一个方法必需要么捕获其方法体能够抛出的全部被检查异常, 要么声明它将抛出这些异常。Arcane3 的 main 方法在静态类型为 Type3 的对象上调用了方法 f,但它对 CloneNotSupportedException 和 InterruptedExceptioin 并无做这些处理。那么,为何这个程序能够编译呢?
  上述分析的缺陷在于对“Type3.f 能够抛出在 Type1.f 上声明的异常和在 Type2.f 上声明的异常”所作的假设。这并不正确,由于每个接口都限制了方法 f 能够抛出的被检查异常集合。一个方法能够抛出的被检查异常集合是它所适用的全部类型声明要抛出的被检查异常集合的交集,而不是合集。所以,静态类型为 Type3 的对象上的 f 方法根本就不能抛出任何被检查异常。所以,Arcane3能够毫无错误地经过编译,而且打印 Hello world。

谜题3: 不受欢迎的宾客

下面的程序会打印出什么呢?

public class UnwelcomeGuest {
    public static final long GUEST_USER_ID = -1;
    private static final long USER_ID;

    static {
        try {
            USER_ID = getUserIdFromEnvironment();
        } catch (IdUnavailableException e) {
            USER_ID = GUEST_USER_ID;
            System.out.println("Logging in as guest");
        }
    }

    private static long getUserIdFromEnvironment() 
        throws IdUnavailableException {
        throw new IdUnavailableException();
    }

    public static void main(String[] args) {
        System.out.println("User ID: " + USER_ID);
    }
}

class IdUnavailableException extends Exception {
}

运行结果

UnwelcomeGuest.java:10: variable USER_ID might already have been assigned
            USER_ID = GUEST_USER_ID;
            ^
1 error

结果说明
  该程序看起来很直观。对 getUserIdFromEnvironment 的调用将抛出一个异常, 从而使程序将 GUEST_USER_ID(-1L)赋值给 USER_ID, 并打印 Loggin in as guest。 而后 main 方法执行,使程序打印 User ID: -1。表象再次欺骗了咱们,该程序并不能编译。若是你尝试着去编译它, 你将看到和一条错误信息。

  问题出在哪里了?USER_ID 域是一个空 final(blank final),它是一个在声明中没有进行初始化操做的 final 域。很明显,只有在对 USER_ID 赋值失败时,才会在 try 语句块中抛出异常,所以,在 catch 语句块中赋值是相 当安全的。无论怎样执行静态初始化操做语句块,只会对 USER_ID 赋值一次,这正是空 final 所要求的。为何编译器不知道这些呢? 要肯定一个程序是否能够不止一次地对一个空 final 进行赋值是一个很困难的问题。事实上,这是不可能的。这等价于经典的停机问题,它一般被认为是不可能解决的。为了可以编写出一个编译器,语言规范在这一点上采用了保守的方式。在程序中,一个空 final 域只有在它是明确未赋过值的地方才能够被赋值。规范长篇大论,对此术语提供了一个准确的但保守的定义。 由于它是保守的,因此编译器必须拒绝某些能够证实是安全的程序。这个谜题就展现了这样的一个程序。幸运的是, 你没必要为了编写 Java 程序而去学习那些骇人的用于明确赋值的细节。一般明确赋值规则不会有任何妨碍。若是碰巧你编写了一个真的可能会对一个空final 赋值超过一次的程序,编译器会帮你指出的。只有在极少的状况下,就像本谜题同样, 你才会编写出一个安全的程序, 可是它并不知足规范的形式化要求。编译器的抱怨就好像是你编写了一个不安全的程序同样,并且你必须修改你的程序以知足它。

  解决这类问题的最好方式就是将这个烦人的域从空 final 类型改变为普通的final 类型,用一个静态域的初始化操做替换掉静态的初始化语句块。实现这一点的最佳方式是重构静态语句块中的代码为一个助手方法:

public class UnwelcomeGuest {
    public static final long GUEST_USER_ID = -1;
    private static final long USER_ID = getUserIdOrGuest();
    private static long getUserIdOrGuest() {
        try {
            return getUserIdFromEnvironment();
        } catch (IdUnavailableException e) {
            System.out.println("Logging in as guest");
            return GUEST_USER_ID;
        }
    }

    private static long getUserIdFromEnvironment() 
        throws IdUnavailableException {
        throw new IdUnavailableException();
    }

    public static void main(String[] args) {
        System.out.println("User ID: " + USER_ID);
    }
}

class IdUnavailableException extends Exception {
}

  程序的这个版本很显然是正确的,并且比最初的版本根据可读性,由于它为了域值的计算而增长了一个描述性的名字, 而最初的版本只有一个匿名的静态初始化操做语句块。将这样的修改做用于程序,它就能够如咱们的指望来运行了。总之,大多数程序员都不须要学习明确赋值规则的细节。该规则的做为一般都是正确的。若是你必须重构一个程序,以消除由明确赋值规则所引起的错误,那么你应该考虑添加一个新方法。这样作除了能够解决明确赋值问题,还可使程序的可读性提升。

谜题4: 您好,再见!

下面的程序将会打印出什么呢?

public class HelloGoodbye {
    public static void main(String[] args) {
        try {
            System.out.println("Hello world");
            System.exit(0);
        } finally {
            System.out.println("Goodbye world");
        }
    }
}

运行结果:

Hello world

结果说明
  这个程序包含两个 println 语句: 一个在 try 语句块中, 另外一个在相应的 finally语句块中。try 语句块执行它的 println 语句,而且经过调用 System.exit 来提早结束执行。在此时,你可能但愿控制权会转交给 finally 语句块。然而,若是你运行该程序,就会发现它永远不会说再见:它只打印了 Hello world。这是否违背了"Indecisive示例" 中所解释的原则呢? 不论 try 语句块的执行是正常地仍是意外地结束, finally 语句块确实都会执行。然而在这个程序中,try 语句块根本就没有结束其执行过程。System.exit 方法将中止当前线程和全部其余当场死亡的线程。finally 子句的出现并不能给予线程继续去执行的特殊权限。
  当 System.exit 被调用时,虚拟机在关闭前要执行两项清理工做。首先,它执行全部的关闭挂钩操做,这些挂钩已经注册到了 Runtime.addShutdownHook 上。这对于释放 VM 以外的资源将颇有帮助。务必要为那些必须在 VM 退出以前发生的行为关闭挂钩。下面的程序版本示范了这种技术,它能够如咱们所指望地打印出 Hello world 和 Goodbye world:

public class HelloGoodbye1 {
    public static void main(String[] args) {
        System.out.println("Hello world");
        Runtime.getRuntime().addShutdownHook(
        new Thread() {
            public void run() {
            System.out.println("Goodbye world");
            }
        });
        System.exit(0);
    }
}

  VM 执行在 System.exit 被调用时执行的第二个清理任务与终结器有关。若是System.runFinalizerOnExit 或它的魔鬼双胞胎 Runtime.runFinalizersOnExit被调用了,那么 VM 将在全部还未终结的对象上面调用终结器。这些方法好久之前就已通过时了,并且其缘由也很合理。不管什么缘由,永远不要调用System.runFinalizersOnExit 和 Runtime.runFinalizersOnExit: 它们属于 Java类库中最危险的方法之一[ThreadStop]。调用这些方法致使的结果是,终结器会在那些其余线程正在并发操做的对象上面运行, 从而致使不肯定的行为或致使死锁。

  总之,System.exit 将当即中止全部的程序线程,它并不会使 finally 语句块获得调用,可是它在中止 VM 以前会执行关闭挂钩操做。当 VM 被关闭时,请使用关闭挂钩来终止外部资源。经过调用 System.halt 能够在不执行关闭挂钩的状况下中止 VM,可是这个方法不多使用。

谜题5: 不情愿的构造器

下面的程序将打印出什么呢?

public class Reluctant {
    private Reluctant internalInstance = new Reluctant();
    public Reluctant() throws Exception {
        throw new Exception("I'm not coming out");
    }
    public static void main(String[] args) {
        try {
            Reluctant b = new Reluctant();
            System.out.println("Surprise!");
        } catch (Exception ex) {
            System.out.println("I told you so");
        }
    }
}

运行结果

Exception in thread "main" java.lang.StackOverflowError
    at Reluctant.<init>(Reluctant.java:3)
    ...

结果说明
  main 方法调用了 Reluctant 构造器,它将抛出一个异常。你可能指望 catch 子句可以捕获这个异常,而且打印 I told you so。凑近仔细看看这个程序就会发现,Reluctant 实例还包含第二个内部实例,它的构造器也会抛出一个异常。不管抛出哪个异常,看起来 main 中的 catch 子句都应该捕获它,所以预测该程序将打印 I told you 应该是一个安全的赌注。可是当你尝试着去运行它时,就会发现它压根没有去作这类的事情:它抛出了 StackOverflowError 异常,为何呢?

  与大多数抛出 StackOverflowError 异常的程序同样,本程序也包含了一个无限递归。当你调用一个构造器时,实例变量的初始化操做将先于构造器的程序体而运行[JLS 12.5]。在本谜题中, internalInstance 变量的初始化操做递归调用了构造器,而该构造器经过再次调用 Reluctant 构造器而初始化该变量本身的 internalInstance 域,如此无限递归下去。这些递归调用在构造器程序体得到执行机会以前就会抛出 StackOverflowError 异常,由于 StackOverflowError 是 Error 的子类型而不是 Exception 的子类型,因此 catch 子句没法捕获它。对于一个对象包含与它本身类型相同的实例的状况,并很多见。例如,连接列表节点、树节点和图节点都属于这种状况。你必须很是当心地初始化这样的包含实例,以免 StackOverflowError 异常。

  至于本谜题名义上的题目:声明将抛出异常的构造器,你须要注意,构造器必须声明其实例初始化操做会抛出的全部被检查异常。

谜题6: 域和流

下面的方法将一个文件拷贝到另外一个文件,而且被设计为要关闭它所建立的每个流,即便它碰到 I/O 错误也要如此。遗憾的是,它并不是老是可以作到这一点。为何不能呢,你如何才能订正它呢?

static void copy(String src, String dest) throws IOException {
    InputStream in = null;
    OutputStream out = null;
    try {
        in = new FileInputStream(src);
        out = new FileOutputStream(dest);
        byte[] buf = new byte[1024];
        int n;
        while ((n = in.read(buf)) > 0)
            out.write(buf, 0, n);
    } finally {
        if (in != null) in.close();
        if (out != null) out.close();
    }
}

谜题分析
  这个程序看起来已经面面俱到了。其流域(in 和 out)被初始化为 null,而且新的流一旦被建立,它们立刻就被设置为这些流域的新值。对于这些域所引用的流,若是不为空,则 finally 语句块会将其关闭。即使在拷贝操做引起了一个 IOException 的状况下,finally 语句块也会在方法返回以前执行。出什么错了呢?
  问题在 finally 语句块自身中。close 方法也可能会抛出 IOException 异常。若是这正好发生在 in.close 被调用之时,那么这个异常就会阻止 out.close 被调用,从而使输出流仍保持在开放状态。请注意,该程序违反了"优柔寡断" 的建议:对 close 的调用可能会致使 finally 语句块意外结束。遗憾的是,编译器并不能帮助你发现此问题,由于 close 方法抛出的异常与 read 和 write 抛出的异常类型相同,而其外围方法(copy)声明将传播该异常。解决方式是将每个 close 都包装在一个嵌套的 try 语句块中。

下面的 finally 语句块的版本能够保证在两个流上都会调用 close:

try {
    // 和以前同样
} finally {
    if (in != null) {
        try {
            in.close();
        } catch (IOException ex) {
            // There is nothing we can do if close fails
        }
    }

    if (out != null) {
        try {
            out.close();
        } catch (IOException ex) {
            // There is nothing we can do if close fails
        }
    }
}

  总之,当你在 finally 语句块中调用 close 方法时,要用一个嵌套的 try-catch 语句来保护它,以防止 IOException 的传播。更通常地讲,对于任何在 finally 语句块中可能会抛出的被检查异常都要进行处理,而不是任其传播。

谜题7: 异常为循环而抛

下面的程序会打印出什么呢?

public class Loop {
    public static void main(String[] args) {
        int[][] tests = { { 6, 5, 4, 3, 2, 1 }, { 1, 2 },
            { 1, 2, 3 }, { 1, 2, 3, 4 }, { 1 } };
        int successCount = 0;
        try {
            int i = 0;
            while (true) {
                if (thirdElementIsThree(tests[i++]))
                    successCount ++;
            }
        } catch(ArrayIndexOutOfBoundsException e) {
            // No more tests to process
        }
        System.out.println(successCount);
    }
    private static boolean thirdElementIsThree(int[] a) {
        return a.length >= 3 & a[2] == 3;
    }
}

运行结果

0

结果说明
  该程序主要说明了两个问题。

  第1个问题:不该该使用异常做为终止循环的手段!
  该程序用 thirdElementIsThree 方法测试了 tests 数组中的每个元素。遍历这个数组的循环显然是非传统的循环:它不是在循环变量等于数组长度的时候终止,而是在它试图访问一个并不在数组中的元素时终止。尽管它是非传统的,可是这个循环应该能够工做。
  若是传递给 thirdElementIsThree 的参数具备 3 个或更多的元素,而且其第三个元素等于 3,那么该方法将返回 true。对于 tests中的 5 个元素来讲,有 2 个将返回 true,所以看起来该程序应该打印 2。若是你运行它,就会发现它打印的时 0。确定是哪里出了问题,你能肯定吗? 事实上,这个程序犯了两个错误。第一个错误是该程序使用了一种可怕的循环惯用法,该惯用法依赖的是对数组的访问会抛出异常。这种惯用法不只难以阅读, 并且运行速度还很是地慢。不要使用异常来进行循环控制;应该只为异常条件而使用异常。为了纠正这个错误,能够将整个 try-finally 语句块替换为循环遍历数组的标准惯用法:

for (int i = 0; i < test.length; i++)
    if (thirdElementIsThree(tests[i]))
        successCount++;

若是你使用的是 5.0 或者是更新的版本,那么你能够用 for 循环结构来代替:

for (int[] test : tests)
    if(thirdElementIsThree(test))
        successCount++;

  第2个问题: 主要比较"&操做符" 和 "&&操做符"的区别。注意示例中的操做符是&,这是按位进行"与"操做。

相关文章
相关标签/搜索