Java学习之旅第三季-2:异常处理机制之抛出异常
文接上节,上一节我主要介绍了异常处理中的捕获异常,且捕获的异常都是JVM 在运行出错时自动抛出的异常,如 ArithmeticException 或 ArrayIndexOutOfBoundsException。本小节我将继续介绍开发人员如何自己在某种条件成立时手动抛出异常。
2.1 throw 关键字
在开发中,除了显式处理异常外,还有些情况下,自身无法处理或不适合处理异常,就可以将异常向上抛出。因为Java程序的执行是从main方法开始,然后一层一层调用方法。比如main方法中调用了方法a,方法a中又调用了方法b,这就形成了所谓的方法调用栈。向上抛出异常就是本方法无法处理的异常直接抛给调用该方法的上层方法。如:方法b中的异常不处理,抛给方法a来处理,当我所有的方法(包括main方法)都不处理,则直接由 JVM 抛出。
下面举个例子:
/*** 计算两个整数值商** @param num1 第一个整数* @param num2 第二个整数* @return 两个整数之商,保留整数* @throws Exception 当第二个整数是0时,抛出此异常*/public int divide(int num1, int num2) throws Exception {if (num2 == 0) {throw new Exception("第二个参数不能为 0");}return num1 / num2;
}
这个方法用于接收两个整型参数,并返回它们的商。不过方法体做了判断第 2 个参数是否为 0,如果为 0 则使用 throw 抛出 Exception 的实例。
关于 throw 关键字的用法,有几点说明:
- 它用于在方法中直接抛出异常对象,要么直接使用 new 关键实例化异常对象,要么抛出已存在的异常对象
- 它后跟的只能是Throwable或其子类的实例,不能是其它类型实例,否则会有编译错误
- 方法执行到throw语句时,会直接终止该方法的执行,代码会跳转到调优该方法的上层方法;如果是main方法,直接交给JVM,JVM直接终止执行且出处异常信息
- 如果抛出的是受检异常(Exception及其除了RuntimeException外的子类),则需要显式处理:
- 在方法后增加 throws 子句,用于声明该方法可能抛出的异常类型,如本例。
- 使用try-catch结构,再次抛出异常实例,如果还是受检异常,则无法避免在方法后增加 throws 子句
- 如果抛出的是运行时异常,则无需显示处理,如:
public int divide(int num1, int num2) {if (num2 == 0) {throw new RuntimeException("第二个参数不能为 0");}return num1 / num2;
}
2.2 throws 关键字
throws 关键字用于在方法声明时指定该方法可能会抛出的异常,通常是受检异常,因为运行时异常在语法上可以不用处理;throws 后可以有多个异常,它们使用逗号分隔,顺序上随意。
上一个例子,我们已经见到由于在方法中使用 throw 抛出受检异常,所以采用了在方法后增加throws。如果是方法中可能抛出多个异常,其写法如下:
public void process(String fileName,String sql) throws IOException, SQLException {if (fileName == null || fileName.isEmpty()) {throw new IOException("IO操作失败");}if(sql==null || sql.isEmpty()){throw new SQLException("SQL执行失败");}System.out.println(fileName);System.out.println(sql);
}
上面的方法模拟了一个及处理IO操作又有数据库操作的方法,其中在对参数进行了空值校验后抛出了两个受检异常IOException,SQLException。抛出这样的异常并不合适,暂且不论,目前主要关注语法。
在调用使用的throws的方法时,也有两种情况:
- 如果方法声明时使用throws抛出了受检异常,则调用者必须显式处理。比如要调用上面的 process 方法可以这样,直接将异常声明向上抛出,交给调用doProcess方法的上层方法:
public void doProcess() throws SQLException, IOException {process("","");
}
也可以这样处理:
try {process("","");
} catch (IOException | SQLException e) {e.printStackTrace();
}
- 如果方法内部抛出的是运行时异常,无论是否有对应的throws,调用方法都无需显式处理,比如调用divide方法:
public void doDivide() {int result1 = divide(12, 3);try {int result2 = divide(12, 3);} catch (RuntimeException e) {e.printStackTrace();}
}
第2行的调用,没有作任何异常处理,语法是成立的,但是若divide方法抛出了异常,相当于异常直接向上抛,按此写法,最终JVM捕获到异常则程序崩溃。
比较推荐的做法还是要处理运行时异常,这样能保证在 divide 出现异常时,出现能继续往后执行。
2.3 再论方法重写
之前在介绍方法重写时提到重写的语法规则有一条是:子类不能抛出比父类方法更多或范围更大的受检异常(父类异常)。现在来看看这是什么意思。
首先声明一个抽象父类,其中三个抽象方法,有两个抽象方法声明时带有 throws关键字:
public abstract class Parent {public abstract void process1();public abstract void process2() throws Exception;public abstract void process3() throws IOException, SQLException;
}
第一个方法没有什么会抛出异常,第二个方法声明抛出受检异常Exception,第三个方法声明为抛出两个受检异常 IOException,SQLException。
那么在子类中,方法重写时就要注意:
- 第一个方法不能增加任何受检异常在 throws 后;
- 第二个方法可以不用 throws,如果需要,只能使用Exception或其子类型(一个或多个都成立)
- 第三个方法可以不用 throws,如果需要,可以保留其中一个或两个都保留,也可以使用它们的任意子类型(一个或多个都成立)
比如下面的重写都是合法的:
public class Child extends Parent {@Overridepublic void process1() {}@Overridepublic void process2() throws IOException {}@Overridepublic void process3() throws SQLException, SocketException {}
}
其中第三个方法中声明抛出的 SocketException 是 IOException 的子类。
一般情况下,在重写父类的方法时,会保持与父类方法相同的异常声明列表。
2.4 JVM抛出的Error
一般开发中需要抛出或需要捕获的都是Exception及其子类,但在特定条件下,JVM会抛出我们无法处理的Error,常见的Error主要包括:OutOfMemoryError、StackOverflowError、ClassNotFoundException、NoClassDefFoundError、NoSuchFieldError、NoSuchMethodError、UnsupportedClassVersionError
- OutOfMemoryError:当 JVM 因内存不足而无法分配对象,并且垃圾回收器也无法再释放更多内存时,就会抛出此异常。例如:
int[] sarray = new int[Integer.MAX_VALUE];
for (int i = 0; i < sarray.length; i++) {sarray[i] = i;
}
不断往整型数组中增加数据。最终JVM会抛出 OutOfMemoryError。错误信息:Requested array size exceeds VM limit
- StackOverflowError:当应用程序递归过深导致栈溢出时抛出。下面的递归没有结束的时候,一定会导致栈溢出,进而抛出StackOverflowError。
public void method() {method();
}
- NoClassDefFoundError:如果 JVM 或 ClassLoader 实例尝试加载某个类的定义,但找不到该类的定义,则抛出此异常。通常是当前正在运行的类在编译时所使用的类是存在的,但运行时已无法找到该类。比如引用的第三方库版本不正确。
- NoSuchFieldError:如果尝试访问或修改对象的指定字段,而该对象已不再有该字段,则会抛出此异常。通常出现在第三方包的版本不兼容时发生。
- NoSuchMethodError:如果试图调用一个类(无论是静态方法还是实例方法)的特定方法,而该类已不再具有该方法的定义,那么就会抛出此异常。通常出现在第三方包的版本不兼容时发生。
- UnsupportedClassVersionError:当 JVM 尝试读取类文件,并且发现文件中的主版本号和次版本号不被支持时,就会抛出此异常。
2.5 小结
本小节介绍了Java异常处理中手动抛出异常的方法。主要内容包括:1)使用throw关键字手动抛出异常,需注意受检异常必须声明throws或捕获处理;2)throws关键字用于声明方法可能抛出的受检异常,调用者需处理;3)方法重写时子类不能抛出比父类更宽泛的受检异常;4)简要介绍了JVM可能抛出的常见Error类型及其产生场景。