Java中通过异常来处理错误
在c语言阶段,处理异常是一种约定俗称的模式,并没有明确的语法规则。这是容易出现问题的,如果我在一个函数中出现异常用返回值的形式同时调用这个函数的方法,但是调用者有可能是忽略则个函数所产生的异常的(即不去处理返回值),同时即便是处理了,最终形成的c语言代码冗余现象严重,使得代码很难阅读和维护。还有一个很重要的问题就是,c语言仅仅只能处理编译时的异常,但是对于大多数运行的时候的异常不能处理,代码的健壮性不高。
Java中处理异常好的多,因为在java中强制规定了如何处理异常,这也就避免了随心所欲的行为,降低错误处理代码的复杂度,有效的将“正常执行的代码”与“处理错误的代码”隔离开使得代码便于维护和阅读。同时,java中有一套改进错误的恢复机制(即 在运行的时候如何处理错误)当在运行的时候产生了异常时,会抛出异常或者捕获异常从而在运行中得到解决,这种内建的恢复系统从而提高了代码的健壮性。
一、java标准异常:
因为在java中一切都是对象,所以 任何一种异常都可以看作是一个类。在java中JDK已经定几种异常(/错误),他们有的定义在java.lang包里,有的定义在java.util,java.io,java.net包中。正如下图所示,所有的异常(/错误)的超类都是Throwable, Throwable有两个直接的子类分别是error和Exception分别代表者不同程度的错误,这两个区别下面说,Exception和error下有好多子类,在Exception中这些子类又可以分为两种,一种成为运行时异常即RuntimeException ,剩下的子类都是编译时异常。
Throwable:有两个重要的子类:Exception(异常)和 Error(错误),二者都是 Java 异常处理的重要子类,各自都包含大量子类。
(1)Error(错误):无法被程序处理的异常,表示运行应用程序中较严重异常。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题比如内存不足,栈溢出等,JVM会选择线程终止。同时error是不可查的(编译器不会检查),也就是说不需要试图throw error或者捕获这个error。。虽然 ThreadDeath 错误是一个“正规”的条件,但它也是 Error 的子类。
(2)Exception(异常):是可以被程序处理的,异常与代码编写者执行的操作有关。
<1>RuntimeException(运行时异常):RuntimeException和error一样也是不可查的(编译器不会检查是否捕获了这个异常等)。一般执行方法期间抛出但未被捕获的任何子类都无需在 throws 子句中进行声明,因为jvm会自动的抛出。也可以不用捕获,这样知道main(),在程序退出前调用异常printStackTrae()方法。也就是说,当程序中可能出现这类异常,即使没有用try-catch语句捕获它,也没有用throws子句声明抛出它,也会编译通过。
<2>其余的Exception子类是编译时异常:这是可查的异常类,如果存在这种异常,但是没有捕获处理或者throw抛出处理,那么编译器会报错。
Error与Exception区别?
Error这是一种特别严重的异常以至于让我们可以称他们为错误,这种异常是不在我们代码编写人员可控范围之内的,无法用代码的方式处理这种异常,相反,Exception出现的异常时由代码编写错误引起的,这可以被代码处理。
为什么RuntimeException时不可查的和其他的子类是必须查的呢?
因为RuntimeException代表的错误首先应该在代码中检查的错误,比如 数组访问越界。之所以不可见查是因为是一种无法预料的错误,比如使用null值的引用,这只有在运行的时候才能判断出是否使用这个引用,编译的时候是不会检查的。
除了这些标准异常之外,还可以自定义一些异常,这就需要知道异常相关的方法构造器等。
二、处理异常的操作规范(抛出与捕获)
1)抛出
(1)throw
对于可查的异常—编译时异常和自定义的异常—必须使用throw抛出,对于不可查的异常—RuntimeException—可以不明确的使用Throw因为JVM会隐含的抛出对应的异常。
当覆盖父类中某个抛出异常的方法的时候,子类中可以抛出父类中允许抛出的异常或其子异常的对象或者不抛出任何异常。
(2)throws(异常声明)
为了能让捕获者知道需要处理那些异常,要在函数中加上异常说明。即在函数名后面throws 多干异常类名之间逗号隔开。
异常说明也可以应用于抽象类和接口中,这就是预先声明异常,在使用这个方法的时候编译器强制用户处理这个异常,但是有可能这个异常没有真实的抛出,这样做的好处就是如果想修改方法抛出某个异常,这样就不会大面积的修改已有的代码。
异常在继承关系中有所限制,对于构造器而言,子类构造器可以随意的抛出自己想要抛出的异常,但是由于基类构造器可能在子类构造器中以编译器隐含的调用无参构造器或者super方式显示的调用有参构造器,所以,必须要在子类构造器中加入基类的异常说明。
对于一般的方法而言,子类中异常说明对象必须时基类中对应的异常说明对象的子类或者其本,或者没有异常说明。反之错误。
例子如下:
import java.io.IOException;
class BaseballException extends Exception{}
class Foul extends BaseballException{}
class Strike extends BaseballException{}
abstract class Inning{
public Inning() throws BaseballException{}
public Inning(String s) throws RuntimeException, IOException{}
public void event() throws BaseballException{}
public abstract void atBat() throws Strike,Foul;
public void walk(){}
}
class StormException extends Exception{}
class RainedOut extends StormException{}
class PopFoul extends Foul{}
interface Storm{
public void event() throws RainedOut;//
public void rainHard() throws RainedOut;
}
class StormyInning extends Inning implements Storm{
//子类构造器可以自己抛出任何的异常不受父类的约束,但是,由于基类的构造器会在子类的构造器中隐含的被调用或者super()
//调用,所以子类也要有基类对应的构造器的异常说明。但在子类中不能捕获基类的异常。
public StormyInning()throws RainedOut ,BaseballException{ }
public StormyInning(String s) throws RainedOut,RuntimeException, IOException {
//try{
super(s);
//}catch(Exception e){}
}
//基类中没有抛出异常,但是子类中却抛出异常,当用多态情况下,用基类引用子类调用子类的walk()时,因为
//基类没有异常说明,但是子类却抛出异常矛盾。
//public void walk()throws PopFoul{ }
//因为一个方法不能通过抛出的异常类型来区分,所以在这里无论抛出RainedOut还是BaseballException
//实现接口与重载是矛盾的。
//public void event( ) throws RainedOut{ }
//当event()不抛出任何异常,作为重载或者对接口的实现来说都是合理的。
public void event(){ }
public void rainHard() throws RainedOut { }
public void atBat()throws PopFoul { }
}
2)捕获
(1)try——catch
Try(监控区域):如果在方法内部抛出了一个异常,那么这个方法就执行到这个位置结束,但是为了能够让方法抛出异常后尽可能的执行下去,try好处之一可以做到try作用就是将有可能抛出异常的代码放到try块中,如果捕获到异常,那么这个异常就交给下面的catch异常处理程序进行处理但是,在这个异常处理程序中可能处理这个程序,这样代码将从异常处理程序后面继续执行,也可能在异常处理程序中再一次的抛出这个异常,同样的也终止方法继续执行。再者,try还有一个好处就是简化处理异常的代码将处理异常的代码与正常执行的代码分离开。
Catch(异常处理程序):当异常抛出之后,异常处理程序负责搜寻参数与异常类型相匹配(所谓的匹配并不是严格的匹配,而是只要能够向上类型转化就可)的第一个处理程序,当没有匹配的异常类型时就会将这个异常抛出到方法中。当这个程序执行完之后就不会在继续执行,将跳出catch群。执行异常处理机制后面的代码。一般将查类比如Exception 等放到最后,子类放到之前,并且如果try中存在抛出多个相同类型的异常的可能,要么进行自定义异常区分开来,要么用一个catch捕获他。
例子:
public class Demo{
public static void main(String[] args) {
int c , b = 8, a = 0;
System.out.println("begin");
try {
System.out.println("inner try ");
c = b/a;
} catch (Exception e) {
System.out.println("inner catch");
}
System.out.println("end");
}
}
/*
begin
inner try
inner catch
end
*/
(2)Try——catch——finally
Finally:无论是否捕获或处理异常,finally块里的语句都会被执行。当在try块或catch块中遇到return语句时,finally语句块将在方法返回之前被执行。在以下4种特殊情况下,finally块不会被执行:
1)在finally语句块中发生了异常。
2)在前面的代码中用了System.exit()退出程序,jvm退出。
3)程序所在的线程死亡。
4)关闭CPU。
Finally一般的作用就是把除内存之外的资源恢复到他们的初始状态。
在finally中可能存在异常的丢失比如:
一、finally可能会覆盖异常
class fException extends Exception{
public String toString(){
return "fException";
}
}
class gException extends Exception{
public String toString(){
return "gException";
}
}
public class Demo {
void f() throws fException{
throw new fException();
}
void g() throws gException{
throw new gException();
}
public static void main(String[] args) {
try{
Demo lm = new Demo();
try{
lm.f();
}finally{
lm.g();
}
}catch(Exception e){
System.out.println(e);
}
}
}
二、在finally中用return 虽然有异常但是没有输出
public class Demo {
public static void main(String[] args) throws Exception {
try{
throw new Exception();
}finally{
return ;
}}}
执行顺序:
1)当try没有捕获到异常时:try语句块中的语句逐一被执行,程序将跳过catch语句块,执行finally语句块和其后的语句;
2)当try捕获到异常,catch语句块里没有处理此异常的情况:当try语句块里的某条语句出现异常时,而没有处理此异常的catch语句块时,此异常将会抛给JVM处理,finally语句块里的语句还是会被执行,但finally语句块后的语句不会被执行;
3)当try捕获到异常,catch语句块里有处理此异常的情况:在try语句块中是按照顺序来执行的,当执行到某一条语句出现异常时,程序将跳到 catch语句块,并与catch语句块逐一匹配,找到与之对应的处理程序,其他的catch语句块将不会被执行,而try语句块中,出现异常之后的语句 也不会被执行,catch语句块执行完后,执行finally语句块里的语句,最后执行finally语句块后的语句;
(3)何时选择抛出何时选择捕获?
捕获处理一般用于直接与用户打交道遇到了异常情况(比如UI设计中),这种情况不能抛出,再抛的话,就给了用户了。
抛出异常一般的作用是通知到调用者,你代码出了问题,那么这时候就使用抛出处理。在web开发中作用于web sever 与 dao或者 web sever之间的异常提示。
三、异常的特点,与之存在的问题
异常的特点就是:异常的抛出地点与捕获的地点之间间隔的栈层不止一层。
Throw与return区别?
这两者都能将程序停止于某一位置,不能继续走先去,但是区别在于return返回后该方法从栈pop掉,然后将带着返回值到下一层的函数中 调用这个pop掉函数的位置继续执行。但是throw抛出异常之后,可能捕获这个异常的方法在这个抛出异常的下面距离好几层的位置,与return相比,throw从捕获到抛出存在一条链,层层调用,层层抛出最终捕获。
正式这种特点,异常存在的问题:
(1)对于处理一个异常有两种模型,终止模型与恢复模型,但最终大多数选择了终止模型。
(终止模型:一个异常抛出之后,就没有在执行第二遍的可能。
恢复模型:一个异常抛出之后,修正了这个异常,从而成功的执行完第二遍。一般在try快中加入while循环,不断的进入try块知道满意为止。)
之所以选择终止模型,不选择恢复模型原因就是方法之间耦合度太大。 如果捕获一个异常并处理之后,要知道异常抛出的位置,判断这个异常是否修正。但是异常的特点就是可能抛出位置与捕获位置之间间隔的栈层很多,所以在某些方法中要记录抛出位置栈的信息。这增加了代码编写难度和维护的难度。
(2)欢迎补充
四、Throwable一些API
构造器:
Throwable( )
Throwable(String message, Throwable cause)
Throwable(Throwable cause)实现异常链
String getMessage()
String getLocalizedMessage()
String toString()
Void PrintStackTrace( ) 重载形式:参数可以为空,为PrintStream 或者 java.io.PrintWriter
Throwable fillInStackTrace( )
getStackTrace( )