兰小伟:Java中的异常传播(一)
授权声明
本文经作者兰小伟首发于中生代架构(ArchThink)
兰小伟:腼腆内秀的“80后Java码农”,艺名:益达,IT圈摸爬滚打5年6载,现于国美金融打杂谋生,业余著有拙作《Solr权威指南》上下册。爱学习乐分享,开源精神的拥趸。自知天资愚钝,故仍砥砺前行!
责编:姜新城
第 2 篇架构好文:2844字 | 6分钟阅读
Java ⾥的异常
Java ⾥的异常其实是⼀种信号,该信号表明了在代码执⾏过程中发⽣的⼀些重要的或未预测到的情况。举个例⼦,⽐如⼀个被请求的⽂件找不到了,或者⼀个数组的索引越界了,⼜或者某个⽹络连接失败了。在代码⾥针对上述情况进⾏显式检查很容易导致代码变得令⼈费解。Java 提供了⼀种异常处理机制来系统性处理诸如此类的错误情况。
这种异常机制是围绕着 try-catch 这种形式来构建的。
throw ⼀个异常就等价于发送了⼀个未预测到的错误情况发⽣了的信号。
catch ⼀个异常是为了采取合适的⽅式来处理这个异常。异常会被异常处理器捕获,在同⼀个上下⽂环境⾥,已经被抛出的异常不会再被捕获。程序运⾏时的⾏为决定了什么类型的异常将会被抛出,以及该如何捕获它们。
throw-catch 原理是嵌⼊在 try-catch-finally 结构⾥。
在 JVM ⾥可以同时执⾏多个线程。每个线程拥有它各⾃的运⾏时栈空间(有时也称之为“栈”或“执⾏栈”),这些栈⽤于协助处理⽅法的执⾏。线程栈⾥的每个元素(这些元素被称之为活跃记录或者栈帧)对应着⼀个⽅法调⽤。每个包含于当前活跃记录的新产⽣的调⽤结果将会被压栈,⽽这些调⽤结果存储了关于线程的本地变量的所有相关信息。这个⽅法连同当前处理栈顶的栈帧⼀起表示着当前的⽅法执⾏。
当这个⽅法执⾏完毕,该⽅法的活跃记录将会被弹出栈。⽽那些处于栈顶仍未覆盖到的活跃记录对应的那些⽅法将会紧接着被执⾏。当该⽅法的调⽤还未执⾏完毕,那么就认为栈中的该⽅法是活跃的。在任何时刻,在运⾏时栈中,这些活跃的⽅法组成了线程执⾏过程的栈轨迹即 stack trace。
下⾯这个简单程序演示了⽅法的执⾏。它计算⼀组 Interger 类型数字的平均值,传⼊所有 Integer 类型数字的总和以及这些数字的总个数。这⾥定义了3个函数:
main函数调⽤ printAverage 函数,printAverage 函数需要传⼊所有 Integer 类型数字的总和以及所有 Integer 类型数字的总个数。(1)
printAverage函数内部会反过来调⽤computeAverage函数。(3)
computeAverage()⽤于计算数字的平均值,并返回计算结果。(7)
public class Average1 {
public static void main(String[] args) {
printAverage(100,0); // (1)
System.out.println("Exit main()."); // (2)
}
public static void printAverage(int totalSum, int totalNumber) {
int average = computeAverage(totalSum, totalNumber); // (3)
System.out.println("Average = " + // (4)
totalSum + " / " + totalNumber + " = " + average);
System.out.println("Exit printAverage()."); // (5)
public static int computeAverage(int sum, int number) {
System.out.println("Computing average."); // (6)
return sum/number; // (7)
}
}
上述程序执⾏后会输出如下结果:
Computing average.
Average = 100 / 20 = 5
Exit printAverage().
Exit main().
java printAverage(100, 20); //(1)
替换为
java printAverage(100, 0); // (1)
然后再次运⾏程序,程序的输出结果将会如下所示:
java Computing average. Exception in
thread "main"java.lang.ArithmeticException: / by zero
at Average1.computeAverage(Average1.java:18)
at Average1.printAverage(Average1.java:10)
at Average1.main(Average1.java:5)
下图演示了上述程序的执⾏流程。代码⼀切执⾏正常,直到运⾏⾄ computeAverage 函数中标记(7)处的语句,在计算 sum/number 这个算式时发⽣了错误,因为拿⼀个数字除以零是⼀个⾮法操作。这个错误是 JVM 通过抛出⼀个 ArithmeticException 异常来发出的。然后 JVM 通过运⾏时栈向上层传播这个异常。
上图演示了异常抛出并且程序未对异常做任何显式处理的情况。在上图中, computeAverage 函数的执⾏在异常抛出点被打断。标记(7)处的 return 语句将永远不会被执⾏。因为该函数并没有对该异常做任何处理,它的执⾏过程也⾃然就被突然中⽌,⽽且与其相关的栈帧也将会被弹出栈。我们将此称之为函数意外结束,然后异常将会被抛⾄当前函数的调⽤者,⽽该调⽤者(这⾥即 printAverage 函数)的栈帧当前将会位于栈顶。
⽽调⽤者的函数⾥也没有对该异常进⾏任何处理,故其函数执⾏过程也将会意外结束。printAverage 函数内标记(4)和(5)⾏的代码将永远不会被执⾏。然后该异常会继续往上传播⾄最后⼀个活跃函数(即 main 函数)。main 函数也没有对该异常进⾏任何处理,故 main 函数也将会意外结束。main 函数内标记(2)⾏的代码也将永远不会被执⾏。由于该异常没有被任何活跃函数所捕获,故它将会被 main 线程的默认异常处理器所处理。
默认的异常处理器通常会打印异常的名称,以及该异常的描述信息,随后通过标准输出流打印该异常被抛出那⼀刻的线程执⾏栈的调⽤轨迹信息。发⽣在线程内部的未捕获异常,会导致该线程死亡。
若在对⼀个⼆进制表达式的左操作数进⾏解析求值时抛出了异常,则右操作数将会跳过不处理。类似的,若对⼀系列表达式(⽐如,⽅法调⽤时实际参数列表)进⾏解析处理时抛出了异常,然后剩下的参数列表将会被跳过不处理。若类似先前展示的终端输出结果的线程执⾏栈的调⽤轨迹信息中不打印⾏号,这时你需要添加⼀个JVM参数,如下所示:
-XX:-OmitStackTraceInFastThrow
(To be continued)