java的异常机制

在程序运行的时候,如果发生了不被期望的事件,它阻止了程序预期的正常执行,这就是异常。

Java异常机制是Java提供的一种识别及响应错误的一致性机制,可以使程序中异常处理代码和正常业务代码分离,保证程序代码更加优雅,并提高程序的健壮性。在有效使用异常处理机制的情况下,异常能清晰地回答WHAT、WHERE、WHY这三个问题:异常类型回答了WHAT被抛出,异常堆栈跟踪回答了在WHERE抛出,异常信息则回答了WHY会抛出,在发生异常的时候能保证程序能尽可能地恢复正常并继续执行。

Java异常的分类和类结构

Java标准库内建了一些通用的异常,这些类以Throwable类为顶级父类。Throwable类派生出两个大类,分别是Error(错误)类和Exception(异常)类。Error类及其子类的实例,代表了JVM本身的错误,这些错误不能被程序员通过代码处理,比如JVM内存溢出或不足。而Exception类及其子类的实例,则代表程序运行时发生的各种不期望发生的事件,是可以被Java异常处理机制使用的。由Java程序员通过代码处理这些Exception异常,是Java异常处理的核心。

总体上我们根据javac对异常的处理要求,可以将异常类分为两类。

非检查异常(Unchecked Exception):Error类、RuntimeException类及它们的子类。javac在编译的时候,不会提示和发现这样的异常,也不要求在程序里处理这些异常。当然如果愿意,我们可以编写代码处理(使用try...catch...finally)这样的异常,也可以不处理。事实上,对于这样的异常,我们应该去修正代码,而不是去通过异常处理器去处理,因为这样的异常发生的原因多半是出于代码写得有问题,比如除0异常ArithmeticException、强制类型转换错误ClassCastException、数组下标越界ArrayIndexOutOfBoundsException、使用了空对象NullPointerException等等。

检查异常(Checked Exception):除了Error类和RuntimeException类的其他异常类。javac强制要求程序员为这样的异常做预备处理工作(使用try...catch...finally或者throws)。在方法中,要么使用try-catch语句捕捉异常并处理,要么使用throws子句声明抛出它,否则编译不会通过。这样的异常一般是由程序的运行环境导致的,因为程序可能被运行在各种未知的环境下,而程序员无法干预用户如何使用他编写的程序,于是程序员就应该为这样的异常时刻准备着,比如SQLException、IOException、IOException和ClassNotFoundException等。

需要明确的是,检查和非检查是对于javac(编译器)来说的,这样就很好理解和区分了。

异常的抛出机制

异常是在执行某个方法时引发的,而函数又是层级调用,形成调用栈的,因此只要一个方法发生了异常,那么他的所有caller都会被异常影响。当这些被影响的方法以异常信息输出的时候,就形成了异常追踪栈。

这里演示一个产生除0异常ArithmeticException的代码,帮助我们理解异常的抛出机制。

public class AllDemo {
    public static void main (String[] args ) {
        System.out.println("----欢迎使用命令行除法计算器----") ;
        cmdCalculate();
    }
      
    public static void cmdCalculate() {
        Scanner scan = new Scanner(System.in);
        int num1 = scan.nextInt() ;
        int num2 = scan.nextInt() ;
        int result = devide(num1 , num2);
        System.out.println("result:" + result) ;
        scan.close() ;
      }
      
    public static int devide(int num1, int num2) {
        return num1 / num2 ;
    }
}

当devide方法的第二个参数num2为0的时候,就会抛出下图的异常信息:

异常最先发生的地方,叫做异常抛出点。当devide()方法发生除0异常的时候,devide()方法将抛出ArithmeticException异常,因此调用它的cmdCalculate()方法也无法正常完成,也会抛出异常,而cmdCalculate()方法的caller(调用者),即main方法,也会因为cmdCalculate()方法抛出异常而抛出异常。就这样一直向调用栈的栈底回溯。这种行为叫做异常的冒泡,是为了在当前发生异常的方法或者这个方法的caller中找到最近的异常处理程序。由于上面的代码中没有使用任何异常处理机制,因此异常最终由main方法抛给JRE,导致了程序的终止运行。

异常处理的基本语法

在发生零除异常的代码中不使用异常机制,也可以顺利编译,是因为这个异常是非检查异常。但是如果是检查异常,则必须要使用异常处理机制处理异常,否则是无法通过编译的(.java源文件编译成.class可执行文件)。而在编写代码处理异常时,对于检查异常是有两种不同的处理方式的,一种是在方法体中使用try...catch...finally语句块,另一种则是在方法签名中使用throws子句声明交给方法调用者(caller)去解决。除此之外,还可以使用throw语句去手动抛出异常。

try...catch..finally语句块

try {
  // try块中放可能发生异常的代码。
  // 如果执行完try且不发生异常,则接着去执行finally块和finally后面的代码(如果有的话)。
  // 如果发生异常,则尝试去匹配catch块。
} catch(SQLException SQLexception) {
  // 每一个catch块用于捕获并处理一个特定的异常,或者这异常类型的子类。Java7中可以将多个异常声明在一个catch中。
  // catch后面的括号定义了异常类型和异常参数。如果异常与之匹配且是最先匹配到的,则虚拟机将使用这个catch块来处理异常。
  // 在catch块中可以使用这个块的异常参数来获取异常的相关信息。异常参数是这个catch块中的局部变量,其它块不能访问。
  // 如果当前try块中发生的异常在后续的所有catch中都没捕获到,则先去执行finally,然后到这个函数的外部caller中去匹配异常处理器。
  // 如果try中没有发生异常,则所有的catch块将被忽略。
 
} catch(Exception exception) {
  // 我是上面那个异常的爸爸。
} finally{
  // finally块通常是可选的。
  // 无论异常是否发生,异常是否匹配被处理,finally都会执行,因此finally块适合用作资源释放操作。
  // 良好的编程习惯是:在try块中打开资源,在finally块中释放资源,比如关闭文件、数据库连接等。
  // 一个try至少要有一个catch块,否则,至少要有1个finally块。但是finally不是用来处理异常的,finally不会捕获异常。   // finally主要做一些清理工作,如流的关闭,数据库连接的关闭等。 }

使用try..catch...finally语句块要注意以下几点:

1.try块中的局部变量和catch块中的局部变量(包括异常变量),以及finally中的局部变量,它们之间不可共享使用。

2.每个catch块用于处理一个异常。异常匹配是按照catch块的顺序从上往下寻找的,只有第一个匹配的catch会得到执行。匹配时,不仅运行精确匹配,也支持父类匹配,因此如果同一个try块下的多个catch异常类型有父子关系,应该将子类异常放在前面,父类异常放在后面,保证每个catch块都有存在的意义。

3.在Java中,异常处理的任务就是将执行控制流从异常发生的地方转移到能够处理这种异常的地方去。也就是说,当一个方法的某条语句发生异常时,这条语句后面的代码不会再执行,它失去了焦点。执行流跳转到最近匹配的异常处理catch代码块去执行,当异常被处理完之后,执行流才会接着在处理了这个异常的catch代码块的后面接着执行。有的编程语言在异常被处理后,控制流会恢复到异常抛出点接着执行,这种策略叫做恢复式异常处理模式(Resumption Model Of Exception Handling);而Java则是让执行流恢复到处理了异常的catch块后接着执行,这种策略叫做终结式异常处理模式(Termination Model Of Exception Handling)。

public static void main(String[] args) {
    try {
        foo();
    } catch(ArithmeticException ae) {
        System.out.println("好像听到有人说想被执行?");
    }
}

public static void foo() {
        int a = 5/0;  // 异常抛出点
        System.out.println("我好想被执行,可是。。");  // 不会被执行
}

throws子句方法声明

如果一个方法内部的代码会抛出检查异常(Checked Exception),而方法自己又没有办法完全处理掉,javac允许在方法的签名上使用throws关键字声明这些可能抛出的异常,保证编译能通过。

throws子句是另外的一种处理异常的方式,它不同于try...catch..finally语句块,仅仅是将方法中可能出现的异常向调用者(caller)声明,而自己不做具体的处理。采取这种异常处理的原因可能是因为方法本身不知道该如何去处理这样的异常,或者说让调用者去处理会更好。而调用者则需要为这些可能发生的异常买单。

public void foo() throws ExceptionType1, ExceptionType2, ExceptionTypeN { 
     // foo内部可以抛出ExceptionType1,ExceptionType2,ExceptionTypeN类的异常,或者他们的子类的异常对象。
}

throw异常抛出语句

程序员也可以通过throw语句手动显式地抛出一个异常,比如哪天心情不好就给他手动抛出个异常让程序炸掉(手动滑稽)。

throw语句的后面必须是一个异常对象,即throw exceptionObject。

throw语句必须写在方法中,执行throw语句的地方就是一个异常抛出点,它和由JRE自动形成的异常抛出点没有任何差别。

public void runnable(Programmer programmer) {
    if ("差".equals(programmer.getEmotion())) {
        throw new RuntimeException("程序员心情不好,不想让程序跑了。");
    }
    
    // ...
}

异常的链化

在一些大型的,模块化的软件开发中,一旦一个地方发生异常,则如骨牌效应一样,将导致一连串的异常。假设B模块完成自己的逻辑需要调用A模块的方法,如果A模块发生异常,则B模块也将不能完成而发生异常。但是B模块在抛出异常的时候会将A模块的异常信息掩盖掉,使得异常的根源信息丢失。采用异常的链化可以将多个模块的异常串联起来,保证异常信息不会丢失。

异常链化是以一个异常对象为参数来构造新的异常对象。新的异常对象叫包含先前异常的信息。这项技术主要是通过异常类的一个带Throwable参数的方法来实现的。这个当作参数的异常被称为根源异常(cause)。

public class Throwable implements Serializable {
    private Throwable cause = this;
 
    public Throwable(String message, Throwable cause)  {
        fillInStackTrace();
        detailMessage = message;
        this.cause = cause;
    }
     
    public Throwable(Throwable cause)  {
        fillInStackTrace();
        detailMessage = (cause==null ? null : cause.toString());
        this.cause = cause;
    }
 
    // ...
}

查看Throw源码,可以发现里面有一个Throwable字段cause,就是它保存了构造Throwable对象时传递的根源异常参数。这种设计和链表的结点类设计如出一辙,因此自然就形成异常链了。

public static void main(String[] args) {
    System.out.println("请输入2个加数");
    int result;
    try {
        result = add();
        System.out.println("结果:" + result);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

// 获取输入的2个整数返回
private static List<Integer> getInputNumbers() {
    List<Integer> nums = new ArrayList<>();
    Scanner scan = new Scanner(System.in);
    try {
        int num1 = scan.nextInt();
        int num2 = scan.nextInt();
        nums.add(new Integer(num1));
        nums.add(new Integer(num2));
    } catch(InputMismatchException immExp) {
        throw immExp;
    } finally {
        scan.close();
    }
    return nums;
}
 
// 执行加法计算
private static int add() throws Exception {
    int result;
    try {
        List<Integer> nums =getInputNumbers();
        result = nums.get(0) + nums.get(1);
    } catch(InputMismatchException immExp) {
        throw new Exception("计算失败", immExp);  // 链化:以一个异常对象为参数构造新的异常对象
    }
    return result;
}

这就是异常的链化。

自定义异常

如果要自定义异常类,则需要扩展Exception类,因此自定义异常一般都属于检查异常(Checked Exception)。当然也可以自定义非检查异常,扩展自RuntimeException。

public class IOException extends Exception {
    
    static final long serialVersionUID = 7818375828146090155L;
 
    public IOException() {
        super();
    }
 
    public IOException(String message) {
        super(message);
    }
 
    public IOException(String message, Throwable cause) {
        super(message, cause);
    }
 
    public IOException(Throwable cause) {
        super(cause);
    }
}

自定义异常用需要另外的篇章去展开说,这里就不详细说了,看看上面的IOException就好了。

异常的注意事项

1.当子类重写父类的带有throws声明的方法的时候,其throws声明的异常必须在父类异常的可控范围内。用于处理匪类的throws方法的异常处理器,也必须使用于子类的这个带throws的方法。这时为了支持Java的多态。例如,父类方法throws的是2个异常,子类就不能throws3个或以上的异常。父类throws IOException,子类就必须throws IOException或IOException的子类。

class Father {
    public void start() throws IOException {
        throw new IOException();
    }
}
class Son extends Father {
    public void start() throws Exception {
        throw new SQLException();
    }
}

这时候Son类是会产生检查异常而导致编译不通过的。为什么呢,我们来看一个场景。

Son son = new Father();
try {
    son.fuck();
} catch (IOException ioe) {
    // 处理异常
}

因为son.fuck()中抛出的是SQLException异常,IOException无法处理这个异常,这里的try...catch语句就没法处理Son.fuck()中的异常,多态就没有办法实现了。

2.Java程序是可以多线程的。每一个线程都是一个独立的执行流,独立的方法调用栈。如果程序只有一个线程,那么没有被任何代码处理的异常都会导致程序终止。如果是多线程的,那么没有被任何代码处理的异常仅仅会导致异常所在的线程结束。也就是说,Java中的异常是线程独立的,线程的问题应该由线程自己解决,而不应该委托到外部,也不应该直接影响到其它线程的执行。

"其实我们都一样不安。"

原文地址:https://www.cnblogs.com/yanggb/p/10389260.html