文章目录
1. 引言
1.1 为什么要提出异常
在异常的概念出现之前,我们通常使用条件控制或syserror的方式来处理异常,并且程序执行过程中一旦出现错误,正序就会报错并停止,这样代码就非常不健壮。
为此,许多高级语言都引入了异常处理机制,此机制可能处理出现的异常而不会停止整个服务,让代码变得更健壮。
异常链
程序会在捕获一个异常后抛出另外一个异常,并且希望把异常原始信息保存下来,这被称为异常链。
1.2 Java的异常
java语言在设计之初,就提出了异常处理的框架,并且在1.4版本后增加了异常链机制,从而便于跟踪异常。Java语言允许你划定异常出现的范围,并给出修正机会,使得程序不因异常而终止或发生其他改变,并且Java能够保存异常信息,通过异常信息能够快速定位问题所在。
掌握异常处理可以让代码更健壮和易于维护,是程序员必须掌握的技能。下面是Java异常的一般定义形式:
try{
// 可能出现异常的代码块
}catch(Exception e){
// 出现异常才会进入
}finally(){
// 无论如何都会进入
}
那么,程序在执行过程中会出现哪些异常呢? 我们必须要处理吗?
在编写Java代码时,应当尽可能去避免错误和异常发生,但是有不可避免、不可预测的情况则发生在代码执行过程中,我们主要关注运行时出现的异常,并对其进行处理。
2. Java异常机制
2.1 Java异常分类
在 Java 中,所有的异常都有一个共同的祖先 java.lang
包中的 Throwable
类。Throwable
类有两个重要的子类 Exception
(异常)和 Error
(错误)。Exception
能被程序本身处理(try-catch
), Error
是无法处理的(只能尽量避免)。
Error
Error是程序无法处理的,比如内存溢出OutOfMemoryError、线程死亡ThreadDeath等,出现错误一般交由JVM处理,而JVM一般会终止线程。
Exception
Exception是程序可以处理的异常,它又分为受检异常(CheckedException)、UncheckedException(不受检异常)。
- CheckedException一般发生在程序编译阶段,代码编译时可以检查出来(如类型错误转换、1/0等)、,必须要使用异常机制来处理,如try-catch来处理。
- UncheckedException发生在运行期,具有不确定性,主要是由于程序的逻辑问题所引起的(比如在文件输入流时突然删除需要的文件),难以排查。
下面是Java源码的异常框架图:
实际上,由于Java具有继承特性,使用者还可以根据需求进行异常自定义
自定义异常:
public class GlobalException extends RuntimeException {
@Getter
@Setter
private String msg;
public GlobalException(String message) {
this.msg = message; // 对信息进行增强等
}
}
##2.2 异常处理
2.2.1 try、catch、finally
前面已经提到,java语言将可能出现异常的代码块包括在try
语句中,jvm在程序执行过程中检查异常语句,如果确保出现异常,则会进入符合条件的catch语句
,然后进入finally
语句,注意,无论如何,try
执行完成后都会进入finally
语句。
在以下 3 种特殊情况下,finally
块不会被执行:
-
在
try
或finally
块中用了System.exit(int)
退出程序。但是,如果System.exit(int)
在异常语句之后,finally
还是会被执行 -
程序所在的线程死亡。
-
关闭 CPU。
2.2.2 异常链
Java在1.4时提出了异常链机制,通过此机制能够更好地实现异常跟踪
异常跟踪
Exception类有一个printStackTrace()方法,它能够从发生异常的方法中输出堆栈信息,默认输出位置是System.err。但是我们现在需要追踪异常,Java允许我们将异常信息包装并重新抛出到其他位置,比如String字符串中,然后我们就可以在任何地方使用此字符串。
2.2.3 throw
Java允许使用者创建一个异常并抛出,从而使程序更健壮。
try {
int a = 0;
if(a == 0){
throw new RuntimeException();
}
}catch (Exception e){
System.out.println("a不能为0");
}
2.2.4 try-with-resources
面对必须要关闭的资源,我们总是应该优先使用 try-with-resources
而不是try-finally
。随之产生的代码更简短,更清晰,产生的异常对我们也更有用。try-with-resources
语句让我们更容易编写必须要关闭的资源的代码,若采用try-finally
则几乎做不到这点。
//读取文本文件的内容
Scanner scanner = null;
try {
scanner = new Scanner(new File("D://read.txt"));
while (scanner.hasNext()) {
System.out.println(scanner.nextLine());
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
if (scanner != null) {
scanner.close();
}
}
// java7之后
try (Scanner scanner = new Scanner(new File("test.txt"));资源2;资源3) {
while (scanner.hasNext()) {
System.out.println(scanner.nextLine());
}
} catch (FileNotFoundException fnfe) {
fnfe.printStackTrace();
}
2.3 如何使用异常
异常处理的目标
-
恢复
-
报告
异常处理的一般逻辑
- 如果自己知道怎么处理异常,就进行处理;
- 如果可以通过程序自动解决,就自动解决;
- 如果异常可以被自己解决,就不需要再向上报告。
3. 异常的底层原理
3.1 异常是怎么抛出的?
public class Test_3 {
public static void main(String[] args) {
try{
int a = 1 / 0;
}catch (Exception e){
e.printStackTrace();
}
}
}
我们可以看到,在最下面出现一个异常表,这就是JVM用来处理异常的,如果在[0,4)中发生异常的话,直接进到语句7,然后调用异常处理方法。
因为如果 JVM 中一个方法编译后的代码正好是 65535 字节长,并且以一条 1 字节长的指令结束,那么该指令就不能被异常处理机制所保护。
-
如果出现异常了,JVM 会在当前的方法中去寻找异常表,查看是否该异常被捕获了。
-
如果在异常表里面匹配到了异常,则调用 target 对应的索引下标的指令,继续执行。
-
如果在异常表里没匹配到异常,jvm直接抛出此因异常,而不调用任何一个catch子句中的代码
3.2 finally为什么一定会执行?
public class MainTest {
public static void main(String[] args) {
try {
int a = 1 / 0;
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println("final");
}
}
}
上面可以看到,一个带有 finally 子句的 try 语句被编译为有一个特殊的异常处理程序,这个异常处理程序可以处理在 try 语句中抛出的(any)任何异常。
在源码中,只在 finally 代码块出现过一次的输出语句,在字节码中出现了三次。
finally 代码块中的代码被复制了两份,分别放到了 try 和 catch 语句的后面。也就是无论是否发生异常、发生什么异常, finally 语句一定会被执行的效果。
finally中为什么不能return?
public class Test_3 {
public static void main(String[] args) {
try{
int a = 1 / 0;
}finally {
System.out.println(222);
return;
}
}
}
// 当finally中书写return时,异常并不会被抛出
其实已经一目了然了。
右边的 finally 里面有 return,并没有 athrow 指令,所以异常根本就没有抛出去。
这也是为什么建议大家不要在 finally 语句里面写 return 的原因之一。
3.3 异常日志是怎么打印的?
上面这个异常日志是怎么打印的呢?
我们找到这个方法,可以看到这个方法只能被JVM调用,并且也可以通过重写此方法。
// java.lang.Thread#dispatchUncaughtException
/**
* Dispatch an uncaught exception to the handler. This method is
* intended to be called only by the JVM.
*/
private void dispatchUncaughtException(Throwable e) {
getUncaughtExceptionHandler().uncaughtException(this, e);
}
public void uncaughtException(Thread t, Throwable e) {
if (parent != null) {
parent.uncaughtException(t, e);
} else {
Thread.UncaughtExceptionHandler ueh =
Thread.getDefaultUncaughtExceptionHandler();
if (ueh != null) {
ueh.uncaughtException(t, e);
} else if (!(e instanceof ThreadDeath)) {
System.err.print("Exception in thread \""
+ t.getName() + "\" ");
e.printStackTrace(System.err);
}
}
}
public class Test_3 {
public static void main(String[] args) {
Thread.currentThread().setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println(2222+e.getMessage());
}
});
int a = 1 / 0;
}
}
3.4 异常语句中的return
public static int test(){
try {
int a = 1/0;
return 1; // 不会执行,发生异常时直接catch了
}catch (Exception e){
return 2; // 返回2
}
}
public static int test(){
try {
int a = 1/0;
return 1; // 不会执行,发生异常时直接catch了
}catch (Exception e){
// 编译器报错,因为没有返回语句
}
}
public static int test(){
try {
int a = 1/1;
return 1; // 返回1,没发生异常,不会执行catch
}catch (Exception e){
return 2;
}
}
public static int test(){
try {
int a = 1/0;
return 1;
}finally {
return 3; // 返回3,异常不会处理,但也不会抛出,与class字节码相关
}
}
public static int test(){
try {
int a = 1/0;
return 1;
}catch (Exception e){ // 异常被捕获
return 2;
}finally {
return 3; // 返回3,
}
}