1 异常概述
1.1 什么是异常
当我们编写程序时,有时会遇到一些意外情况或错误,这些情况超出了我们预期的正常情况。比如,我们可能会在程序中除以零,试图打开一个不存在的文件,或者因为网络问题无法连接到服务器。
int n = 10;
int m = 0;
int r = n / m; // ArithmeticException: / by zero
这些意外情况就被称为异常。当程序遇到异常时,如果异常没有被捕获和处理,程序可能会崩溃或者出现错误的结果。
可以把异常比作现实生活中的意外情况。当你驾驶汽车时,你预期能够平稳地行驶,但如果突然出现了一个障碍物,你就需要采取一些措施来应对这个意外情况,比如刹车或者绕道而行。同样地,程序在执行过程中也会遇到意外情况,需要通过异常处理来应对。
因此,异常可以被认为是程序执行过程中的意外情况或错误,需要通过适当的处理措施来应对,以确保程序能够继续执行或提供适当的响应。
1.2 异常类型继承关系
Java中一切都是对象,异常消息也不例外,也是对象。异常对象的类型之间存在继承关系。所有的异常类都是从Throwable类派生而来的。Throwable是Java异常类层次结构的根类,它分为两个主要的子类:Error和Exception。
1. Error类:Error类表示严重的错误,通常是无法恢复的错误,例如内存溢出、系统崩溃等。程序一般无法处理这类错误,因为它们通常表示了底层系统的问题,所以我们不需要在代码中捕获或处理Error。
2. Exception类:Exception类表示一般的异常情况,它又分为两种类型:
- 检查异常(Checked Exception):这些异常在编译时就需要进行处理,否则编译器会报错。受检异常通常表示外部条件的变化或输入错误,例如文件未找到、网络连接中断等。开发者必须在代码中显式地捕获和处理受检异常,或者在方法签名中声明抛出该异常,以便调用方处理。
- 非检查异常(Unchecked Exception):这些异常在编译时不需要进行处理,因为它们通常表示程序内部错误、逻辑错误或者编程错误。非受检异常也称为运行时异常(RuntimeException)。它们可以在代码中捕获和处理,但也可以选择不处理。常见的非检查异常包括空指针异常、数组越界异常等。
异常类之间的继承关系形成了异常类的层次结构。派生自Exception类的异常又可以进一步派生出其他异常类,形成更具体的异常类型。这样的层次结构允许我们在捕获异常时进行精确的处理,以便适应不同的异常情况。
总结起来,Java的异常类型继承关系如下所示:
Throwable
├── Error
└── Exception
├── Checked Exception
└── Unchecked Exception (RuntimeException)
这种继承关系帮助我们理解异常类的分类和处理方式,以便在编写代码时更好地处理异常情况。
1.3 Throwable类的API
Throwable类是Java中所有错误和异常的根类,它是Exception和Error类的父类。Throwable类提供了一些常用的API方法,用于处理和获取有关错误和异常的信息。下面是Throwable类的一些常用API方法:
- getMessage():获取异常的详细描述信息。返回一个字符串,描述异常的原因或相关信息。
- getCause():获取导致当前异常的原因。返回一个Throwable对象,表示导致当前异常的异常或错误。
- printStackTrace():打印异常的堆栈跟踪信息。将异常的堆栈跟踪信息输出到标准错误流,用于调试和错误诊断。
- getStackTrace():获取异常的堆栈跟踪信息。返回一个StackTraceElement数组,包含异常发生时方法的调用链信息。
- toString():返回异常对象的字符串表示。通常返回异常类的名称,以及异常的详细描述信息。
总的来说,Throwable类提供了处理和获取异常信息的基本方法,可以帮助开发人员更好地理解和处理错误和异常情况。
2 处理异常
2.1 try-catch语句块
使用try-catch语句块可以让我们捕获和处理异常,避免程序崩溃或出现不受控制的错误。它使我们能够在程序执行过程中遇到异常时,采取适当的措施,保证程序的正常运行,并提供错误信息或容错机制。这样可以提高程序的健壮性和可靠性,增加用户体验。
try-catch语句块是一种用于处理异常的结构,在Java中被广泛使用。它由两个主要部分组成:try块和catch块。
try块用于包含可能会抛出异常的代码。在try块中,我们可以放置可能会发生异常的语句或代码块。当程序执行到try块时,它会尝试执行其中的代码。
catch块用于捕获并处理try块中可能抛出的异常。如果try块中的代码执行过程中发生了异常,那么异常将被catch块捕获,并根据catch块中的逻辑进行相应的处理。catch块提供了异常处理的代码逻辑。
try-catch语句块的基本语法如下:
try {
// 可能会抛出异常的代码
} catch (异常类型1 变量名1) {
// 异常处理逻辑1
} catch (异常类型2 变量名2) {
// 异常处理逻辑2
} catch (异常类型3 变量名3) {
// 异常处理逻辑3
}
当程序执行try块中的代码时,如果发生了与catch块中定义的异常类型匹配的异常,那么程序将跳转到匹配的catch块,并执行相应的异常处理逻辑。catch块可以有多个,每个catch块可以捕获不同类型的异常,并提供相应的处理逻辑。
2.2 try-catch示例
下面是使用try-catch语句块的示例代码:
public class ExceptionDemo2 {
public static void main(String[] args) {
System.out.println("程序开始了");
Scanner console = new Scanner(System.in);
System.out.print("请输入一个字符串:");
String str = console.nextLine();
str = "null".equals(str) ? null : str;
try {
// 当字符串为null时,会出现空指针异常
System.out.println(str.length());
// 当字符串为""时,会出现下标越界异常
System.out.println(str.charAt(0));
// 当字符串不是数字时,会出现数字格式化异常
System.out.println(Integer.parseInt(str));
// 当出现异常时,后面的代码不会再执行
System.out.println("!!!!!!!");
} catch (NullPointerException e) {
System.out.println("出现了空指针!");
} catch (StringIndexOutOfBoundsException e) {
System.out.println("出现了下标越界!");
} catch (Exception e) {
// Exception是所有异常的父类,所以它能捕获所有异常
System.out.println("反正就是出了个错!");
}
// 异常处理后,程序会继续执行
System.out.println("程序结束了");
}
}
在上述示例中,我们使用了try-catch语句块来捕获可能发生的异常。
在try块中,我们执行了三个可能抛出异常的操作:获取字符串长度、获取字符串的第一个字符、将字符串转换为整数。针对每个可能的异常,我们使用了对应的catch块来捕获并处理异常。在catch块中,我们打印出相应的错误信息。
如果没有发生异常,则try块中的所有代码都会被顺序执行。如果发生了异常,程序会跳转到匹配的catch块,并执行相应的异常处理逻辑。在示例中,我们使用了不同的catch块来处理不同类型的异常,最后一个catch块使用了Exception来捕获所有异常。如果异常没有被任何catch块捕获到,那么程序将终止,并打印出异常的调用栈信息。
无论是否发生了异常,程序都会继续执行try-catch语句块之后的代码,因此在最后打印出"程序结束了"。通过使用try-catch语句块,我们能够在程序执行过程中捕获和处理异常,以确保程序的正常执行,并提供适当的错误信息。
2.3 finally语句块
finally语句块在try或catch语句块之后,不论try语句块中是否出现异常,finally语句块中的代码均会被执行。主要用于执行释放资源的逻辑。
根据Java的异常机制,如果某行代码出现异常,Java会自动触发异常机制,跳转到异常处理逻辑中。此时,出现异常的那行代码后面的代码将不会被执行。也就是说,try语句块并不能保证其中的所有代码都被执行。
在一些场景中,一段程序的最后需要执行一些“收尾”操作,例如重置计数器、删除不再使用的数组或集合、关闭流等。如果将这些“收尾”代码写在try语句块的末尾,当出现异常时,这些代码将不会被执行,从而影响程序的正常逻辑或导致资源得不到释放等问题。
为了解决这个问题,Java引入了finally语句块。无论是否发生异常,finally语句块中的代码都会被执行。这样,我们可以将“收尾”代码放在finally语句块中,确保在任何情况下都能执行这些代码,以实现资源的释放和清理操作。
使用finally语句块可以保证在异常处理后进行必要的清理工作或执行其他必要的操作,从而确保程序的一致性和可靠性。
2.4 finally示例
下面是使用finally语句块的示例代码:
public class ExceptionDemo3 {
public static void main(String[] args) {
System.out.println("程序开始了");
try {
String str = null;
System.out.println(str.length());
} catch (Exception e) {
System.out.println("出错了!");
} finally {
System.out.println("finally中的代码执行了!");
}
System.out.println("程序结束了");
}
}
在上述示例中,我们使用了try-catch-finally结构来处理可能发生的异常。
在try块中,我们执行了一个可能引发空指针异常的操作:获取一个null引用的长度。由于str为null,所以会触发空指针异常。
在catch块中,我们捕获并处理了异常,打印出"出错了!"。
无论异常是否被捕获,finally块中的代码都会被执行。在示例中,我们使用了finally块来打印出"finally中的代码执行了!"。无论try块中的代码是否抛出了异常,这段代码都会被执行,确保了在异常处理后的一致性操作。
最后,在try-catch-finally结构之后,我们打印出"程序结束了"。通过使用finally语句块,我们可以确保在程序执行过程中进行清理工作或执行必要的操作,无论是否发生了异常。
2.5 finally的执行时机
finally语句块中的代码一定会被执行,那么它是在什么时间点执行的呢?这个细节常作为面试中的考题出现。例如下列代码中,get()的返回结果应该是多少?
public static int get1(){
int a =1;
try{
return a+1;
}finally {
a = 3;
}
}
以上代码中,get1方法的返回结果为2。可以看到,finally代码虽然会被执行,但是在try中的return后面的表达式运算后执行,所以方法返回值是在finally执行前就确定好了。
如果在finally中也增加一个return:
public static int get2(){
int a =1;
try{
return a+1;
}finally {
a = 3;
return a;
}
}
以上代码中,get2方法的返回结果为3,这里的finally中多了一个return语句,影响了整个方法的返回结果。换句话说,如果finally中也有return语句,那么整个方法的返回结果就是finally中的return的值。
如果在try/catch中JVM突然中断了(如使用了System.exit(0)),那么finally中的代码还会执行吗?
public static void print(){
try{
System.out.println("try");
System.exit(0);
}finally {
System.out.println("finally");
}
}
输出结果为:"try"。finally的执行需要两个前提条件:对应的try语句块被执行,程序正常运行。当使用System.exit(0)中断执行时,finally就不会再执行了。
除了我们提到的这种System.exit(0)中断执行的情况,还有几种情况也会导致finally不会执行:
- System.exit()方法被执行
- try或catch语句块中有死循环
- 操作系统强制“杀掉了”JVM进程,如执行了kill -9命令
需要注意的是,这些情况都属于极端情况,通常在正常的程序执行中,finally语句块都会被执行。