什么是异常?
异常是Java语言中的一部分,它代表程序中由各种原因引起的“不正常”因素。 那么在程序中什么样的情况才算不正常呢? 我认为可以这样定义:如果出现了这么一种情况,它打断了程序期望的执行流程,改变了控制流的方向(包括让JVM停掉),那么就可以认为发生了不正常情况,也就是引发了异常。举个例子显而易见的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
FileOutputStream out = null ; try { out = new FileOutputStream( "abc.text" ); out.write( 1 ); System.out.println( "写入成功" ); } catch (FileNotFoundException e) { System.out.println( "要写入的文件不存在" ); e.printStackTrace(); } catch (IOException e) { System.out.println( "发生了IO错误" ); e.printStackTrace(); } finally { if (out != null ){ try { out.close(); } catch (IOException e) { e.printStackTrace(); } } } |
我调用FileOutputStream.write(int)方法期望向一个文件写入一个字节的数据,如果在写入时发生了IO错误, 那么就发生了“不正常情况”,也就是抛出IOException,进而程序的控制流发生了改变,本来如果写入成功的话, 会执行FileOutputStream.write(int)下一句代码, 现在发生了异常, 那么程序要跳到IOException对应的catch块中,去处理这个异常情况。
异常体系和分类
Java以面向对象的方式来管理异常情况,也就是说,Java程序执行时遇到的各种问题都被封装成了对象,并且这些对象之间具有继承关系。java中的让人不爽的“不正常情况”可以分为两种,一种叫做Error,一种是在程序中到处可见的Exception,而他们都继承自Throwable。Exception又分为编译时受检查异常(Checked Exception)和运行时异常(RuntimeException)。如下图所示(该图片来源于网络):
一般情况下,Error代表虚拟机在执行程序时遇到严重问题,不能再回复执行了,这属于重大事故,虚拟机要挂掉的,一句话概括就是“这病没得治,等死就行了”。那么打开JDK的文档,列举几种Error:
VirtualMachineError: 当 Java 虚拟机崩溃或用尽了它继续操作所需的资源时,抛出该错误。
ClassFormatError:当 Java 虚拟机试图读取类文件并确定该文件存在格式错误或无法解释为类文件时,抛出该错误。
NoClassDefFoundError:当 Java 虚拟机或 ClassLoader 实例试图在类的定义中加载,但无法找到该类的定义时,抛出此异常。
而相对于Error,Exception是java程序中遇到的“不那么严重”的问题,这种问题是可以处理的,当处理了这个问题后,程序还可以继续执行。一句话概括,“这是病,得治,这病是可以治好的”。
Exception就比较常见了,随便举几个例子。当创建文件输入流时, 发现文件不存在,那么抛出FileNotFoundException,但是异常可以处理,没法读文件,并不会在很大长度上影响整个程序的执行,毕竟不能读文件,程序还可以执行其他逻辑。下面举一个趣味性的示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
|
public class Travel { private static int power = 100 ; private static boolean bridgeIsOk = true ; public static void main(String[] args) { //描述一下坐火车旅游的过程 System.out.println( "从济南出发, 到北京旅游" ); System.out.println( "列车开到德州" ); //中途给妈妈打个电话 try { telToMom(); } catch (BatteryDiedException e){ System.out.println( "换一块电池, 继续旅程" ); } //桥断了 if (!bridgeIsOk){ System.out.println( "旅程结束" ); throw new BridgeBreakError( "桥断了,列车停止运行" ); } System.out.println( "到北京站,下车" ); //下雨了 try { throw new RainException( "下雨了" ); } catch (RainException e){ System.out.println( "撑起准备的雨伞, 继续旅程" ); } } private static void telToMom() throws BatteryDiedException{ if (power == 0 ){ //手机电量为0 System.out.println( "手机没电了" ); throw new BatteryDiedException( "手机没电了" ); } System.out.println( "给妈妈打电话" ); } static class BatteryDiedException extends Exception{ public BatteryDiedException(String msg){ super (msg); } } static class BridgeBreakError extends Error{ public BridgeBreakError(String msg){ super (msg); } } static class RainException extends Exception{ public RainException(String msg){ super (msg); } } } |
上面的代码描述了一次旅行, 如果在旅途中给妈妈打电话,发现手机没电了, 抛出BatteryDiedException,但是这种异常是可以应付的,直接换一块准备的备用电池就OK了,下了车之后,天下雨了,抛出RainException,这种异常也可以应付,因为提前准备了雨伞。这两种情况都是可以恢复的,遇到之后,只需做一定的处理,旅程还能继续。如果在途中遇到桥断裂的情况,那么列车必须停止运行,这次旅行就泡汤了,也就是说已经不能从这种恶劣情况中恢复过来,所以直接抛出BridgeBreakError。
编译时受检查异常和运行时异常
那么再说一下编译时受检查异常和运行时异常。回顾一下异常的定义:程序在执行时遇到的不正常情况。那么既然是运行时遇到的问题,怎么还有一个编译时受检查异常呢?其实编译时根本不会发生异常,只会在语法错误的情况下编译失败,但是这和异常是不相关的概念。异常只是运行时的行为。那么编译时受检查异常又是一个什么概念呢?要理解受检查异常存在的意义,那么必须明确编码者所处的位置,也可以说编码者的角色, 即:我是功能的具体实现者, 还是功能的使用者,也可以说,我是方法的编写者还是已有方法的调用者。如果我是方法的实现者,我在编码时发现可能会出现异常,那么首先我要明确,这个可能出现的异常我能不能自己处理,如果能自己处理, 那么就在方法内部自己处理掉,如果不能自己处理,那么通知方法的调用者处理。举例说明:
1
2
3
4
|
public static Class<?> forName(String className) throws ClassNotFoundException { return forName0(className, true , ClassLoader.getCallerClassLoader()); } |
上面的代码是JDK中Class类的forName()方法。作为JDK类库的作者,在写这个方法的时候,可能会出现异常, 也就是类加载不到。但是他不知道如何处理这个情况,因为他不知道调用这个方法的用户是加载的什么类,可能是一个非常重要的类, 加载不成的话程序就只能停掉,也可能是一个不那么重要的类,加载不到也没有严重影响。所以,如何处理这个情况,必须是由用户决定。方法后面的throws ClassNotFoundException的意义是:这个方法可能出现ClassNotFoundException,你如果调用了这个方法,那么必须做好防范措施(用try-catch处理这个异常,或者再向上抛出)。如果站在方法使用者的角度,我调用这个方法,如果出现异常,我可以提前准备好解决方案:
1
2
3
4
5
6
7
8
|
try { Class clazz = Class.forName( "com.bjpowernode.Person" ); } catch (ClassNotFoundException e) { System.out.println( "Person类加载失败" ); System.exit( 0 ); e.printStackTrace(); } |
Person类是一个非常中要的类,必须加载成功才能继续执行。如果加载失败, 只能让程序停掉,并且打印出日志。这样的话,程序员可以在其他地方确保这个类必须是可加载的。
所以,可以把编译时受检查异常看做一种错误预警机制:这个错误可能发生, 但也可能不发生,但是如果你想使用这个功能的话,必须做好处理措施,可以使用try-catch处理异常, 也可以抛向更高层。
说完了编译时受检查异常,那么在谈运行时异常, 所有运行时异常的顶层父类都是RuntimeException, RuntimeException也是继承自Exception的。下面是JDK文档中对运行时异常的解释。
1.RuntimeException 是那些可能在 Java 虚拟机正常运行期间抛出的异常的超类。
3.可能在执行方法期间抛出但未被捕获的 RuntimeException 的任何子类都无需在 throws 子句中进行声明。
也就是说, 如果你在方法中抛出了运行时异常或者其子类,那么可以不必在方法上声明会抛出异常,所以调用这个方法的调用者也就不必在使用的时候做预防措施。那么在异常发生的时候,由于没有处理措施,那么只能让虚拟机停掉,也就是说这种异常一般不需要提前预防。那么什么时候使用运行时异常呢?可以这样认为:如果发生了这样一个异常时,让程序停掉是合理的,那么这种情况就适合使用运行时异常。
还是以上面旅行的例子做一个说明。如果手机在旅途没电了,那么预防这种情况是有意义的,因为换了电池之后还可以继续旅行;突然下雨这种情况也可预防,并且预防这种情况是有意义的,因为打起伞来同样可以继续前进。那么,如果如果在旅途中病了,并且病的还很厉害,那么再预防这种情况对整个旅程来说就没有什么意义了,因为旅程必须终止(看病要紧)。所以直接抛出一个运行时异常让旅程终止。如下:
1
2
3
4
5
6
7
8
9
10
11
12
|
private static boolean isSick = true ; public static void main(String[] args) { if (isSick){ System.out.println( "生病了,旅途中止" ); throw new SickException( "病了" ); } } private static class SickException extends RuntimeException{ public SickException(String msg){ super (msg); } } |
一般来说,运行时异常非常适合处理编程错误,那么什么是编程错误呢?可以认为是程序员写的代码有问题,必须修改程序才能解决问题。看一下JDK中的两个RuntimeException的例子。
IllegalArgumentException:如果用户(方法的调用者)传递的参数不对,那么就会抛出非法参数异常,然后让程序停掉,如果想让程序正确的运行,必须修改调用方式,传递一个正确的参数。如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
public static void main(String[] args) { caculateSalary( 3 ); } /** * 计算一个月的薪资 * @param month 月份 */ public static void caculateSalary( int month){ //如果参数错误, 抛出非法参数异常 if (month < 1 || month > 12 ){ throw new IllegalArgumentException(); } } private static void caculateSalaryInner( int month){ //计算薪资 ... } |
NullPointerException:如果调用一个方法的对象为null,那么在调用的时候会抛出空指针异常。如果要避免的话,就要修改程序,确保调用方法的对象不为空。
ClassCastException:如果在进行类型转换时,指定了错误的目标类型,那么会抛出类型转换异常。如果要避免的话,要修改代码,以确保指定了正确的要转换的目标类型。
虽然RuntimeException一般用于表示编程错误,在抛出运行时异常时让程序停掉,对代码做一定的改正以让程序可以再次正确运行, 但是要注意到,运行时异常是可以捕获的,捕获之后做出处理后,程序可以恢复执行:
1
2
3
4
5
6
7
8
9
10
11
|
public static void main(String[] args) { doSomething(); } public static void doSomething(){ Object obj = null ; try { //运行时异常也是可以捕获的 obj.toString(); } catch (RuntimeException e) { System.out.println( "抛出了运行时异常, 异常的具体类型:" + e.getClass().getName()); } } |
打印结果为: 抛出了运行时异常, 异常的具体类型:java.lang.NullPointerException
另外,运行时异常也可以在方法上声明抛出,但是如果方法上声明的是运行时异常,那么方法的调用者可以选择处理, 也可以选择不处理。如果不处理的话,程序会终止,如果捕获后做出处理,程序可以恢复运行:
1
2
3
4
5
6
7
8
|
public static void main(String[] args) { doSomething(); //不必处理方法声明抛出的运行时异常 } public static void doSomething() throws RuntimeException{ throw new RuntimeException(); } |
虽然运行时异常可以在方法上声明抛出,也可以被捕获,但是一般情况下我们不会这么做。因为运行时异常一般用于表示编程错误,出现异常时让程序停掉是合理的。对运行时异常进行捕获和声明抛出没有多大的意义。比如捕获了空指针异常,虽然进行了处理以让程序不至于崩溃,但是空对象要调用的方法,根本就没有调用成功,这是不合理的。
如何合理使用异常
上面介绍了异常的定义和分类,也提到了一些异常的使用原则。现在总结一下到底应该如何使用异常:
1 重大的错误使用Error。一般Error用于表示系统级别的或虚拟机层面上的错误,在编程中很少使用。
2 有必要预防,并且处理后可以让程序恢复执行的情况使用编译时受检查异常。
3 编程错误使用运行时异常。
4 如果方法自己可以处理异常,那么可以选择自己处理异常,如果方法不知道如何处理异常,那么抛给高层的方法调用者。
5 方法声明抛向高层的异常,必须是对高层有意义并且高层能够理解的异常。
下面再举一个趣味性的例子。
老板派员工出去执行一项任务,在这个过程中有两个角色,员工是低层被调用者,老板是高层调用者。在这个过程中可能出现这么几种情况。
1 老板让员工出去执行一项任务, 那么必须得给拨款(没钱干不成事嘛)。那么如果老板没给钱,或者给的钱不够,那么员工可以选择停止执行。这属于编程错误,要求老板必须给足够的钱才能继续运行。这种情况使用运行时异常表示。
2 到了目的地后,要去办公地点,发现迷路了(可能方向感不好,转向了),找不到公交车的站牌了。这个错误自己完全可以解决,打个车就可以了。并且不能抛给老板,如果抛给老板,那么就等着被炒鱿鱼吧。老板每天很忙,他会这样认为:这员工太操蛋了,这点事都办不成。所以这是个受检查异常,并且适合在内部解决。
3 出差在外,加班太辛苦,在干到一半的时候,累病了。这就是比较严重的情况了,自己不能很好地解决(得去医院)。这也是可以预见的异常,毕竟人都会得病嘛。这也属于受检查异常,自己不能解决,得抛向高层(老板)。但是应该怎样给老板说呢?不能给老板说“老板,我病了”,如果这样给老板说的话,老板会一头雾水:“病了去医院啊, 我又不是医生”。那么怎么给老板说呢?直接告诉老板任务不能完成就行了,当然可以说明为什么不能完成的原因(生病了)。这样的话,老板就可以做出一些处理,可以另外再拍一个人去交接任务,也可以决定暂停任务。所以,抛向高层的异常,必须是对高层有意义并且高层能够理解的异常。
下面用代码描述这个过程:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
public class DoWork { public static class Boss{ //老板 private Employee emp; //员工对象 public Boss(Employee emp){ this .emp = emp; } public void doWork(){ try { emp.doWork(); //老板委托员工外出执行任务,给员工块钱的经费 } catch (TaskCannotCompleteException e) { //任务无法完成 System.out.println( "派出另一个员工去完成任务" ); } } } public static class Employee{ //员工 //执行任务,可能不能完成任务 public void doWork( float money) throws TaskCannotCompleteException{ // if (money < ){ //经费太少,无法执行任务 throw new MoneyNotEnoughException(); } // try { goToWorkPlace(); } catch (CannotFindBusException e) { //在去工作地点时找不到公交车 System.out.println( "打车去" ); } // try { workDayAndNight(); } catch (TiredToSickException e) { //累病了 //告诉老板,任务无法完成 throw new TaskCannotCompleteException(); } } //在去工作地点时可能找不到公交车 private void goToWorkPlace() throws CannotFindBusException{ //throw new CannotFindBusException(); } //没天没夜的干活, 可能会累病 private void workDayAndNight() throws TiredToSickException{ //throw new TiredToSickException(); } } //找不到公交车异常 public static class CannotFindBusException extends Exception{} //经费不足异常 public static class MoneyNotEnoughException extends RuntimeException{} //累病异常 public static class TiredToSickException extends Exception{} //任务无法完成异常 public static class TaskCannotCompleteException extends Exception{} public static void main(String[] args) { Boss boss = new Boss( new Employee()); boss.doWork(); } } |