两类异常
Java中的异常继承体系为:
这里的异常( Exception)分为两大类:Checked异常和Runtime异常。所有的RuntimeException
类及其子类的实例被称为Runtime异常;不是RuntimeException
类及其子类的实例则被称为Checked异常。
Java认为Checked异常都是可以被处理(修复)的异常,所有Java程序必须显式处理Checked异常。如果程序没有处理Checked异常,则该程序无法通过编译(这是Checked异常与Runtime异常最大的不同)。
Checked异常体现了Java的设计哲学——没有完善处理的代码根本不会被执行!
对Checked异常的处理有两种方式:
- 如果当前方法明确知道如何处理该异常,程序应该使用
try...catch
块来捕获异常,然后在对应的catch
块中处理该异常; - 如果当前方法不知道如何处理该异常,则在定义方法时必须声明抛出异常。
提示:在Eclipse中,可以使用快捷键对Checked异常进行处理,即选择try...catch
块来捕获异常或者throws
声明抛出异常。
使用throws声明抛出异常
使用throws
声明抛出异常的思路为:当前方法不知道如何处理该异常,该异常应该由上级调用者处理;如果main
方法也不知道如何处理该异常,也可以使用throws
声明抛出异常,将该异常交给JVM处理。JVM处理异常的方法则简单粗暴得多:打印异常的跟踪栈信息,并中止程序运行。
throws
声明抛出只能在定义方法时使用,如果要抛出多个异常类,多个异常类用逗号隔开:
throws ExceptionClass1, ExceptionClass2...
下面是一个简单的例子:
public class ThrowTest {
public static void main(String[] args) throws IOException {
FileInputStream fis = new FileInputStream("a.txt");
}
}
由于程序所在的目录中没有文件“a.txt”,而且main
方法使用throws
声明抛出异常,将异常交给JVM处理,所以程序遇到了异常后便打印异常的跟踪栈信息,并结束程序:
如果某段代码中调用了一个带throws
声明抛出的方法,该方法声明抛出了Checked异常,则表明该方法希望它的调用者来处理该异常。也就是说,调用该方法时要么放在try
块中显式捕获该异常,要么放在另一个带throws
声明抛出的方法中。看下面的例子:
public class ThrowTest2 {
public static void main(String[] args) throws Exception {
test();
}
public static void test() throws IOException {
FileInputStream fis = new FileInputStream("a.txt");
}
}
对两个throws
声明抛出的解释是:因为test()
方法声明抛出IOException
异常,所以调用该方法的代码要么处于try...catch
块中,要么处于另一个带throws
声明抛出的方法中;因为FileInputStream
的构造器声明抛出IOException
异常,所以调用该方法的代码要么处于try...catch
块中,要么处于另一个带throws
声明抛出的方法中。
使用throws
声明抛出异常时同样有限制:
- 子类方法声明抛出的异常应该是父类方法声明抛出的异常类型的子类或相同;
- 子类方法声明抛出的异常不允许比父类方法声明抛出的异常多。
Checked异常的优劣
Checked异常有两大不便之处:
- 对于Checked异常,Java程序的处理更加复杂,增加了编程的复杂度;
- 如果在方法中显示声明抛出Checked异常,将会导致方法签名与异常耦合,如果该方法是重写父类方法,则该方法抛出的异常还会受到限制。
当然,既然Checked异常存在,必然有其合理性:Checked异常能在编译时提醒程序员代码可能存在的问题,提醒程序员必须注意处理该异常,或者声明该异常由该方法的调用者来处理,从而可以避免程序员因为粗心而忘记处理该异常。
使用throw抛出异常
异常是一种很“主观”的说法,很多时候系统是否要抛出异常,可能根据应用的业务需求来决定。这时候如果因与业务需求不符而产生异常,只能有程序员自行决定抛出,系统是无法抛出的(因为并不是“客观”意义上的异常)。
throw
语句抛出的不是异常实例,而是一个异常实例,而且每次只能抛出一个异常实例:
throw ExceptionInstance;
来看下面的例子:
try {
int b = 0;
if (b == 0) {
throw new Exception("除数不能为零!");
}
} catch (Exception e) {
System.out.println("除数不能为零!");
}
当Java程序运行时接收到开发者自行抛出的异常时,同样会中止当前的执行流,跳到该异常对应的catch
块,由catch
块来处理该异常。
throw
语句抛出的异常如果是Checked异常,则该throw
语句要么处于try
块里,显式捕获该异常,要么放在一个带throws
声明抛出的方法中,把该异常交给该方法的调用者处理;如果是Runtime异常,则该语句无须放在try
块里,也无须放在带throws
声明抛出的方法中。程序既可以显式使用try...catch
来捕获并处理该异常,也可以完全不理会该异常,把它交给该方法的调用者处理。来看下面的例子:
public class ThrowTest {
public static void main(String[] args) {
try {
//调用声明抛出Checked异常的方法
//要么显式捕获该异常
//要么在main方法中再次声明抛出
throwChecked(3);
} catch (Exception e) {
System.out.println(e.getMessage());
}
//调用声明抛出Checked异常的方法
//既可以显式捕获该异常
//也可以不理会该异常
throwRuntime(4);
}
public static void throwChecked(int a) throws Exception {
if (a > 0) {
throw new Exception("a的值大于0,不符合要求!");
}
}
public static void throwRuntime(int a) {
if (a > 0) {
throw new RuntimeException("a的值大于0,不符合要求!");
}
}
}
程序抛出异常:
上图中第一句话是Checked异常的详细描述,而下面三行红字则是Runtime异常的跟踪栈信息。
注意:throw
与throws
虽然只相差一个字母,用法却千差万别,使用时一定要注意区分!
自定义异常类
通常情况下,程序很少自行抛出系统异常(因为这样没有必要)。在抛出异常时,应该选择合适的异常类,从而可以明确地描述该异常情况。在这种情形下,程序需要抛出自定义异常。
自定义异常类都应该继承Exception
类,如果希望自定义Runtime异常,则应该继承RuntimeException
类。自定义异常类时需要提供两个构造器:一个是无参的构造器,另一个带一个字符串参数的构造器,这个字符串作为该异常对象的描述信息(也就是异常对象的getMessage()
方法的返回值):
public class AuctionException extends Exception{
//无参的构造器
public AuctionException(){
};
//带一个字符串参数的构造器
public AuctionException(String msg){
//通过super关键字调用父类的构造器
//将此字符串参数传给异常对象的message属性
super(msg);
}
}
如果要自定义Runtime异常,只需将上面代码中的父类Exception
改为RuntimeException
。
catch和throw同时使用
在实际应用中,往往需要对异常采取复杂的处理方式——但一个异常出现时,单靠某个方法无法完全处理该异常,必须由几个方法协作才可以完全处理该异常。也就是说,在异常出现的当前方法中,程序只对异常进行部分处理,还有些处理需要在该方法的调用者中才能完成,所以应该再次抛出异常,让该方法的调用者也能捕获到异常。来看下面的例子:
public class AuctionTest {
private double initPrice = 3.0;
public void bid(String bidPrice) throws AuctionException {
double d = 0.0;
try {
d = Double.parseDouble(bidPrice);
} catch (Exception e) {
//控制台打印异常的跟踪栈信息
e.printStackTrace();
//再次抛出自定义异常
throw new AuctionException("竞拍价必须是数值,不能含有其他字符!");
}
if (initPrice > d) {
throw new AuctionException("竞拍价比起拍价低,不允许竞拍!");
}
}
public static void main(String[] args) {
AuctionTest at = new AuctionTest();
try {
at.bid("df");
} catch (AuctionException ae) {
//再次捕获到bid()方法中的异常,并对该异常进行处理
System.err.println(ae.getMessage());
}
}
}
程序抛出异常:
提示:这种catch
和throw
结合使用的情况在大型企业级应用中非常常用。企业级应用对异常的处理通常分成两部分:应用后台需要通过日志来记录异常发生的详细情况;应用还需要根据异常向应用使用者传达某种提示。在这种情形下,所有异常都需要两个方法共同完成,也就必须将catch
和throw
结合使用。
Java的异常跟踪栈
异常对象的printStackTrace()
方法用于打印异常的跟踪栈信息,根据printStackTrace()
方法的输出结果,开发者可以找到异常的源头,并跟踪到异常一路触发的过程。来看下面的例子:
class SelfException extends RuntimeException {
SelfException() {
};
SelfException(String msg) {
super(msg);
}
}
public class PrintStackTraceTest {
public static void main(String[] args) {
firstMethod();
}
public static void firstMethod() {
secondMethod();
}
public static void secondMethod() {
thirdMethod();
}
public static void thirdMethod() {
throw new SelfException("自定义异常信息");
}
}
程序抛出异常:
由上图可知,异常从thirdMethod
方法开始触发,开始传到secondMethod
方法,再传到firstMethod
方法,最后传到main
方法,在main
方法终止,这个过程就是Java的异常跟踪栈。
异常的传播规律是:只要异常没有被完全捕获(包括异常额没有被捕获,或异常处理后重新抛出了新异常),异常从发生异常的方法逐渐向外传播,首先传给该方法的调用者,该方法的调用者再次传给其调用者......直到最后传给main
方法,如果main
方法依然没有处理该异常,JVM会中止该程序,并打印异常的跟踪栈信息。
上图的异常跟踪栈信息十分清晰地记录了程序执行停止的各个点:第一行的信息详细显示了异常的类型和详细信息;接下累跟踪栈记录程序中所有的异常发生点,各行显示被调用方法中执行的停止位置,并表明类、类中的方法名、与故障点对应的文件的行。
下面的例子展示了多线程程序中发生异常的情形:
public class ThreadExceptionTest implements Runnable {
public void run() {
firstMethod();
}
public void firstMethod() {
secondMethod();
}
public void secondMethod() {
int a = 5;
int b = 0;
int c = a / b;
}
public static void main(String[] args) {
new Thread(new ThreadExceptionTest()).start();
}
}
程序抛出异常:
由上图可知,程序在Thread
的run
方法中出现了ArithmeticException
异常,这个异常的源头是ThreadExceptionTest
的secondMethod
方法,这个异常传播到Thread类的
run`方法就会结束。
异常处理规则
- 不要过度使用异常
异常只应该用于处理非正常的情况,不要使用异常处理来代替正常的流程控制。对于一些完全剋可预知、而且处理方式清楚的错误,程序应该提供相应的错误处理代码,而不是将其笼统地称为异常。 - 不要使用过于庞大的
try
块
把大块的try
块分割成多个可能出现异常的程序段落,并把它们放在单独的try
块中,从而分别捕获异常。 - 避免使用
Catch All
语句
所谓Catch All
语句指的是一种异常捕获模块,它可以处理程序发生的所有可能异常。这种处理方式有两个弊端:无法对不同的异常分情况处理(如果要分情况处理,则需要在catch
块中使用分支语句进行控制,得不偿失);容易“压制”异常或者忽略“关键”异常。
Catch All
语句:
try{
//可能引发Checked异常的代码
} catch(Throwable t) {
//进行异常处理
t.printStackTrace();
}
- 不要忽略捕获到的异常
既然捕获到了异常,那catch
块就理应做些有用的事情,即对异常采取适当的措施:处理异常、重新抛出新异常或者在合适的层处理异常。