一、异常的定义
程序运行时,发生的不被期望的事件,它阻止了程序按照程序员的预期正常执行,这就是异常。
二、异常的分类
Error:描述了Java运行时系统内部很少发生的内部系统错误和资源耗尽情况(例如:虚拟机错误,链接错误)。
不要抛出Error对象,因为程序员对此通常无能为力。
常见的Error有:Virtual MachineError(虚拟机运行错误)、NoClassDefFoundError(类定义错误)等。
Exception:程序本身可以捕获并且可以处理的异常。
除此之外异常还分为Unchecked异常和Checked异常。
Unchecked异常:Error+RuntimeException。可以不处理,编译时也没有问题,但执行时出现就导致程序失败,代表程序中的潜在bug。
Checked异常:Exception中除RuntimeException之外的异常。编译器会检查此类异常,必须捕获并指定错误处理器handler,否则编译无法通过。
三、异常的处理机制
声明异常:throws
抛出异常:throw
捕获异常:try,catch,finally
3.1 throws
用于方法声明上,表示该方法会抛出一个异常,但不进行处理,交给该方法的调用者来处理异常。
举例
public class Throws {
public static void main(String[] args) throws IOException{
readFile();
}
public static void readFile() throws IOException {
InputStream is = new FileInputStream("D:/Lab2/ch01.txt");
}
}
在Throws类中,有两个方法。一个是readFile方法,该方法是向指定目录读取一个文件,如果文件读取失败,就会抛出一个IOException。而main方法调用readFile。观察结果:
表示在线程main中出现了异常FileNotFoundException。
3.2 throw
throw: 用在方法内部,表示抛出一个异常,传递给调用者处,并结束方法。
public class Throws {
public static void main(String[] args) {
int a = Throws.div(1,0);
System.out.println(a);
}
public static int div(int a,int b)
{
if(b==0)
throw new ArithmeticException("除数不能为0");
return a/b;
}
}
查看结果
这里要注意throw和throws的区别。
虽然两者都是抛出异常,但是throws用于方法签名上,表示可能会抛出某种异常,而throw用于方法内部,执行throw一定会抛出异常。
并且注意到我们上面讲到的Unchecked异常和Checked异常。如果throw抛出一个Checked异常,要么用try-catch进行捕获处理,要么用throws抛给上级调用者。
3.3 try-catch-finally
try用于监控代码是否会产生异常,若产生异常,就交给catch进行处理。无论是否产生异常,最后都会执行finall语句
try {
//监视代码,一旦返现异常则直接跳转至catch,
// 如果没有异常则直接跳转至finally
} catch (Exception e) {
//如果发现异常则进行处理或向上抛出。
} finally {
//必选执行的代码块,不管是否有异常发生,
}
我们对上面的除零代码进行变化,用try-catch捕获处理。
public class Throws {
public static void main(String[] args) {
try {
int a = Throws.div(1,0);
}
catch(ArithmeticException e){
System.out.println("除数不能为0!");
}
finally {
System.out.println("这是finally语句");
}
}
public static int div(int a,int b)
{
if(b==0)
throw new ArithmeticException();
return a/b;
}
}
执行结果
四、例题
为了更好的理解上面的知识,我们来看一道例题。
这是一道跟异常有关的题目,给出了异常的堆栈跟踪信息。
我们知道方法的调用是遵循堆栈的先进后出的方式。
观察题目中的堆栈跟踪信息,首先告诉我们在main方法中出现了异常CarAlreadyParkingException,之后给了我们四个方法,那么根据堆栈先进后出的方式,我们可以知道,先打印出的方法应该是最后调用的。由此我们可以知道,客户端应该是调用D中的main方法,main方法调用C中的parking方法,C中的parking方法调用B中的parking方法,以此类推。直到A中的parking方法产生了一个异常。
由此我们模拟一下代码
public class MyException extends Exception {//自己定义的异常类
private int detail;
MyException(int a){
detail = a;
}
public String toString(){
return "MyException ["+ detail + "]";
}
}
public class A {//A类
public static void parking() throws MyException{
A.compute(5);
System.out.println("这是A");
}
public static void compute(int a) throws MyException{//当a大于10时会抛出异常
System.out.println("Called compute(" + a + ")");
if(a > 10){
throw new MyException(a);
}
System.out.println("Normal exit!");
}
}
public class B {
public static void parking() throws MyException{
A.parking();
System.out.println("这是B");
}
}
public class C {
public static void parking() throws MyException{
B.parking();
System.out.println("这是C");
}
}
public class D {
public static void main(String [] args) throws MyException{
C.parking();
System.out.println("这是D");
}
}
上面的代码模仿了题目。其中D类的main方法调用C的parking方法,C的parking方法调用B的parking方法,B的parking方法调用A的parking方法,A的parking方法调用compute方法,compute方法在a大于10时会抛出异常。
这里要注意我在每个类的方法调用时,后面打印一句“这是X”,目的是验证方法的调用顺序。
我们首先令a=5,让方法正常运行,查看结果。
我们可以看到,打印出的顺序正好与我们调用方法的顺序相反,这就说明方法的调用是先进后出的方式。
之后我们令a=20,再次查看。
我们发现,这跟题目中给定堆栈跟踪信息类似,说明我们的代码逻辑正确。
对于A选项,我们就知道了异常发生的第一现场应该是打印的第一句,也就是A类的111行。
对于B选项,该异常并不能确定是unchecked异常还是checked异常。
对于C选项,如果该异常是unchecked异常,那么方法签名上就不需要有throws。
对于D选项,该异常可能在被捕获后,再次抛出。
例如
public class D {
public static void main(String [] args) throws MyException{
try {
C.parking();
System.out.println("这是D");
}
catch(MyException e) {
throw e;
}
}
}
在D的main方法中,我们用try-catch捕获异常,但是处理时,只是把异常再次向上抛出。查看结果
与之前的结果一样。
所以该题选B。