Java里的异常分为Checked和Unchecked两类,其中RuntimeException及其派生类被称为Unchecked异常,其余则是Checked异常。
+- Throwable // 可以被throw/catch
|+- Error // 通常表示错误,跟Unchecked异常差不多
|+- Exception // Checked异常
||+- RuntimeException // Unchecked异常
编译器会检查所有抛出的的Checked异常都有被显式处理(throws也算显式处理)。
示例
对于Unchecked异常,使用上跟C#的异常没什么两样:
1 2 3 4 5 6 7
| class UncheckedException extends RuntimeException{} public void Run(){ if( x <= 0){ throw new UncheckedException(); } }
|
而对于Checked异常,如果像上面那样写的话,编译会失败,提示”Unhandled Exception: CheckedException”:
1 2 3 4 5 6 7
| class CheckedException extends Exception{} public void Run(){ if( x <= 0){ throw new CheckedException(); } }
|
要通过编译,可以加上try/catch处理掉这个异常:
1 2 3 4 5 6 7 8
| public void Run(){ try{ if( x <= 0){ throw new CheckedException(); } catch(CheckedException e){ } }
|
也可以在方法后声明throws:
1 2 3 4 5
| public void Run() throws CheckedException{ if( x <= 0){ throw new CheckedException(); } }
|
不过这样的话,调用Run()的方法也必须处理这个异常了:
1 2 3 4 5 6 7 8 9 10
| public void Run2(){ try{ Run(); catch(CheckedException e){ } } public void Run3() throws CheckedException{ Run(); }
|
throws是方法签名一部分吗?
throws和返回类型都是方法的元数据,自然也是方法签名的一部分:
1 2 3
| interface I { void f() throws Exception; }
|
因为方法可以重载,所以我们可以写出同样的名字但参数不同的方法:
1 2 3 4
| class A{ void f(int x){} void f(String x){} }
|
返回类型不同则会编译错误,因为编译器将不知道要调用哪个:
1 2 3 4 5 6 7 8
| class A{ int f(){return 0;} String f(){return "";} } class B{ int f(int x){return 0;} String f(String x){return "";} }
|
throws与返回类型类似:
1 2 3 4 5 6 7 8 9
| class A{ void f() throws Exception{} void f() throws RuntimeException{} } class C{ void f(int x){} void f(String x) throws Exception{} }
|
可以这么认为: 方法名和参数类型确定要调用哪个方法,返回类型跟throws对调用者产生约束。
Checked异常带来的问题
假如原先有这么段代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public class NoPowerException extends Exception {} public class Car{ private int power = 0; public void Run() throws NoPowerException{ if(power <= 0){ throw new NoPowerException(); } } } public class Main { public static void main(String[] args) { Car car = new Car(); try { car.Run(); } catch (NoPowerException e) { } } }
|
后来出现了一种能飞的车,但有重量限制,这时候纠结了……
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| public class TooHeavyException extends Exception{} public class AirCar extends Car { private int weight = 0; @Override public void Run() throws NoPowerException { if(weight > 10){ throw new TooHeavyException(); } super.Run(); } } public class Main { public static void main(String[] args) { Car car = new AirCar(); try { car.Run(); } catch (NoPowerException e) { } } }
|
方法1: 将TooHeavyException改继承自RuntimeException
某一天调用者大呼坑爹:IDE只提示我要处理NoPowerException,这个TooHeavyException哪来的,是愚人节彩蛋吗?
方法2: Car.Run和AirCar.Run的方法后面都加上TooHeavyException的声明
因为子类而修改父类是大忌,Car本来对TooHeavyException一无所知,加这个声明很难说得通,如果Car这个类是第三方或者JDK里的,这方法就走不通了。
方法3: 能飞的还叫Car做啥?我们从零开始造AirCar吧~
代价好高……
方法4: 不叫Run,叫DoRun()吧~
假如Car里面有其他代码调用过Run()方法,那就哭了。这么改下去,代码更难读了……
方法n: 期待各位补充
小结
方法的返回类型和throws都是对调用方的约束,并且这种约束还不能通过继承实现多态的,相比之下,Erlang/Javascript在这方面都没有限制,扩展起来倒是舒服很多。