15 异常

  • Java的基本哲学:写得不好的代码无法运行

15.1 概念

  • C 语言和其他较早的语言往往有多种错误处理机制,这类机制通常是通过约定建立的,而不是作为编程语言的一部分。
  • 异常可以降低错误处理代码的复杂性
    • 如果没有异常,就必须检查某个特定的错误并加以处理,可能要在程序多个地方进行
    • 而异常可以确保有人会捕捉他们,并且只需在一个地方进行处理

15.2 基本的异常

  • 异常情况:指组织当前方法或作用域继续执行的问题.
  • 需要区分普通问题和异常
    • 普通问题:当前上下文有足够的信息解决问题
    • 异常:当前上下文没有足够的信息处理问题,因此需要跳出当前上下文,将问题委托给更上层的上下文
  • 当抛出一个异常时,会发生以下几件事
  1. 异常对象创建,使用new创建,放在堆上
  2. 当前执行路径停止,指向这个异常对象的引用被从上下文中推出
  3. 异常处理机制接管控制,寻找异常处理程序

异常参数

  • 和Java其它对象一样,使用new在堆上创建异常,其会分配存储空间并调用构造器
    • 所有标准异常类有连个构造器,一个无参构造器,第二个接收一个String参数,用于放置在异常中放置的相关信息
if(t == null)
    throw new NullPointerException("t = null");
  • 关键词throw有意思
    • 使用new创建异常对象后,使用throw将其返回,这种返回类似于return,但是不会返回任何值,而是指示程序退出当前执行路径
    • 此外可以抛出任何类型的 Throwable(异常类型的根类),通常错误信息就包含在异常类型的名字中

15.3 捕捉异常

  • 被守护区域:一段可能产生异常的代码,后面跟着处理异常的代码

try 块

  • try块包含了异常可能发生的代码,这样在代码抛出异常后,异常处理机制就能捕获它
  • 用类似C这样不支持异常处理的语言编写的库,需要依赖临时的机制报告错误. 有了异常处理,可以把所有的内容放在一个try块中,并在一个地方捕获所有异常,意味着代码更容易编写和阅读,因为代码的正确执行目的不会被错误检查所混淆
try {
 // 可能会产生异常的代码
}

异常处理程序

  • 异常处理程序紧跟在 try 块之后,用关键字 catch 来表示。
try {
 // 可能会产生异常的代码
} catch(Type1 id1) {
 // 处理 Type1 类型的异常
} catch(Type2 id2) {
 // 处理 Type2 类型的异常
} catch(Type3 id3) {
 // 处理 Type3 类型的异常
}
  • 每个catch子句(异常处理程序)像一个小方法,接收且只接收一个特定类型的参数. 标识符(id1,id2)可以在处理程序中使用,它们引用了抛出异常对象的引用
  • 异常处理程序需要紧跟在try块后,一个异常抛出后,异常处理机制和寻找参数和异常类型相匹配的一个处理程序,然后进入catch子句,我们就认为这个异常被处理

终止与恢复

  • 在异常处理理论中,有两种基本模型

    • 终止模型 termination:异常处理程序不试图修复错误,而是终止程序运行. Java支持终止模型
    • 恢复模型 resumption:异常处理程序负责纠正错误,并重新尝试调用错误产生的方法. 支持恢复模型意味着我们仍然希望在处理完异常后继续执行
  • 如果想在Java中达到恢复模型的效果,可以把try块放在一个while循环中,不断重新进入try块直到结果令人满意

  • 从历史上看,最终仍是使用终止模型,因为恢复模型将导致耦合,使得代码难以编写和维护

15.4 创建自己的异常类

  • 通常,异常类都是继承自Exception类,或者继承自Exception的某个子类
    • 一个异常最重要的是其类名
class SimpleException extends Exception {}

public class InheritingExceptions {
  public void f() throws SimpleException {
    System.out.println(
      "Throw SimpleException from f()");
    throw new SimpleException();
  }
  public static void main(String[] args) {
    InheritingExceptions sed =
      new InheritingExceptions();
    try {
      sed.f();
    } catch(SimpleException e) {
      System.out.println("Caught it!");
    }
  }
}
/* Output:
Throw SimpleException from f()
Caught it!
*/
  • 也可以创建一个接收String参数的异常类
class MyException extends Exception {
  MyException() {}
  MyException(String msg) { super(msg); }//调用父类的构造器,Exception的构造器接收一个String参数,同时Exception继承自Throwable
}

public class FullConstructors {
  public static void f() throws MyException {
    System.out.println("Throwing MyException from f()");
    throw new MyException();
  }
  public static void g() throws MyException {
    System.out.println("Throwing MyException from g()");
    throw new MyException("Originated in g()");
  }
  public static void main(String[] args) {
    try {
      f();
    } catch(MyException e) {
      e.printStackTrace(System.out);//Throwable的printStackTrace()方法,打印异常栈轨迹,这里将其输出到System.out,如果使用无参版本,则会将其输出到System.err
    }
    try {
      g();
    } catch(MyException e) {
      e.printStackTrace(System.out);
    }
  }
}
/* Output:
Throwing MyException from f()
MyException
        at FullConstructors.f(FullConstructors.java:11)
        at
FullConstructors.main(FullConstructors.java:19)
Throwing MyException from g()
MyException: Originated in g()
        at FullConstructors.g(FullConstructors.java:15)
        at
FullConstructors.main(FullConstructors.java:24)
*/

异常和日志记录

  • 可以使用java.util.logging工具将输出记录到日志中

15.5 异常说明

  • 异常声明exception specificaton 为方法声明的组成部分,出现在参数列表之后. 可以语法礼貌地告诉程序员这个方法会抛出的异常
void f() throws TooBig, TooSmall, DivZero { // ...
  • 而以下写法意味着这个方法不会抛出异常(除了从RuntimeExecption继承而来的异常,这样的异常可以从任何地方抛出而不要异常说明)
void f() { // ...
  • Java保证在编译时提供一定程度的异常正确性,即如果方法中出现了异常,要么处理这个异常,要么使用异常说明指明这个异常可能会被抛出
    • 这种在编译时被检查并强制实施的异常叫做检查型异常(checked exception)

15.6 捕捉任何异常

  • 通过捕捉异常类型的基类Exception,可以捕捉任何异常. 使用时需要放到处理列表的最后,避免在其他异常处理程序之前捕获了异常
catch(Exception e) {
 System.out.println("Caught an exception");
}
  • Exception类是所有对程序员很重要的异常类的基类,因此我们不能从他得到太多信息,我们可以通过调用其基类Throwable的方法来获取信息
    • getMessage()方法返回异常对象的详细信息
    • getLocalizedMessage()方法返回异常对象的本地化信息
    • toString()方法返回异常对象的类型和详细信息
    • 以下方法打印Throwable的调用栈轨迹,调用栈显示了异常抛出的地方,以及异常传播的路径
      • printStackTrace(),打印到标准错误流
      • printStackTrace(PrintStream),打印到指定流
      • printStackTrace(PrintWriter),打印到指定流