Java的基本理念”结构不佳的代码不能运行“。
异常处理的一个重要目标是把错误处理的代码同错误发生的地点相隔离。
----------------------------------------------------
一、基本概念
1.为什么要有异常机制
发现错误的最理想时机是在编译阶段,即在我们试图运行程序之前。但是这只是一种理想状态,实际是不可能的,所以我们需要一种机制,将编译期间发现不了的错误发送给接收者,接收者通过分析这个错误做出相应的处理,这种机制应该具有发现错误,收集错误信息,将错误信息发送给接收者并可以继续执行正常的程序。这样的错误机制可以提高软件产品的可用性和健壮性,因为我们并不希望使用一个软件时因为一个错误就直接瘫痪掉。
2.什么是异常的抛出
首先看一下如何抛出一个异常:
throw new NullPointerException();//一个空指针异常
//or
throw new NullPointerException("t=null");
Java中异常同其他类一样,我们在抛出时总需要用new先在堆中创建一个异常对象,就如上边代码。在所有的标准异常类中都有俩个构造器,一个是默认构造器;另一个是带有一个字符串参数的构造器(字符串存放相关的异常信息)。上边的throw关键字主要起到返回的作用(此时创建的异常对象应用会传给throw),它可以将异常对象的引用返回给更大的空间处理,以使得在当前域中不需要花过多精力去处理异常。
比较方法返回机制与throw的异同点:
*相同点:
- 都具有返回的特质;
*不同点:
- 返回类型不同,方法中的返回机制可以返回基本数据类型,也可以返回引用类型的数据。throw只能返回异常的对象引用。
- 返回地点不同,方法中的返回机制只能将数据返回给调用它的上一个方法中。throw可以将异常返回到特别远的地方,它会跨越方法调用栈的许多层次。
二、异常机制的使用
1.异常捕捉
监控区域(guarded region)——一段可能产生异常的代码,并且后面跟着处理这些异常的代码。具体监控形式如下所示:
try
{
//需要捕捉异常的问题代码块
}catch(Exception e)//捕捉异常的对象
{
e.printStackTrace();//打印异常信息
}finally{
//无论异常是否被捕捉到都会被执行的代码
//如可以关闭文件、网络、输入输出流等
}
当然也不是所有的finally代码块都会被执行,例子如下:
class ExceptionOne extends Exception{
public String toString(){
return "this is ExceptionOne!";
}
}
class ExceptionTwo extends Exception{
public String toString(){
return "this is ExceptionTwo!";
}
}
public class TestException{
public static void f() throws ExceptionOne{
throw new ExceptionOne();
}
public static void clean() throws ExceptionTwo{
throw new ExceptionTwo();
}
public static void main(String[] args){
try{
f();
try{
f();
}catch(ExceptionOne eo){
eo.printStackTrace();
}
finally{
clean();
return;
}
}catch(Exception e){
e.printStackTrace();
}
}
}
如上的try-catch嵌套中,如在最外层捕捉到了异常,那么内层的try-catch就会被跳过,包括finally代码块也不会被执行。
--- 注:---
finally虽然在编程中起到了非常重要的作用,但有时也会带来一些异常丢失的问题,例子如下:
public static void main(String[] args){
try{
try{
f(); //此时异常会被finally中的异常覆盖
}finally{
clean();
}
}catch(Exception e){
e.printStackTrace();
}
}
public static void main(String[] args){
try{
try{
f();
}finally{
//clean();
return;
}
}catch(Exception e){
e.printStackTrace();
}
}
此种情况实际是不会产生任何异常信息输出的。
*****
这种丢失异常信息的问题是非常严重的,这使得异常机制如同虚设,虽然在书中提到Java会在未来版本进行关于此问题的修正,但是在JDK1.8版本中仍存在此问题,如今也只能我们开发者多多注意此种代码形式的编写以避免这个问题。
*****
2.异常信息输出
自顶向下的强制执行的异常说明机制,保证Java在编译时就能有一定水平的代码正确性。
Exception是与编程有关的所有异常类的基类(继承了Throwable类,可以通过调用此类的方法获取异常的详细信息),可以用来捕获所有的异常。常用显示异常信息的方法如下:
- String getMessage()——用来获取异常的详细信息;
- String getLocalizedMessage()——用来获取本地语言表示的详细信息;
- String toString()——返回对Throwable的简短描述,要是有详细信息也会包含在内;
- void printStrackTrace()\void printStrackTrace(PrintStream)\void printStrackTrace(java.io.PrintWriter)——显示Throwable和Throwable的调用栈轨迹,调用栈显示了“把你带到异常抛出地点”的方法调用序列。
public class TestException{
public static void main(String[] args){
try{
throw new Exception("This is a Exception");
}catch(Exception e){
System.out.println("Caught a exception");
System.out.println("getMessage():"+e.getMessage());
System.out.println("getLocalizedMessage()"+e.getLocalizedMessage());
System.out.println("toString():"+e);
System.out.println("printStackTrace():");
e.printStackTrace(System.out);
}
}
}
3.重新抛出异常
public void g() throws Exception{
throw new Exception("come from g()");
}
public void f(){
try{
g();
}catch(Exception e){
throw e;//将捕获的g()中的异常重新抛出,此时会把异常抛给调用它的上一级方法的代码中,如main方法调用了就抛给main
}
}
如果只是简单的将捕获的异常重新抛出,之前异常对象的所有信息都会被保持:
1.使用fillInstackTrace()返回一个新的Throwable对象,然后把当前调用栈的信息填入原来那个异常对象,从而更新抛出点的调用栈信息。
public class TestException{
public static void f() throws Exception{ //异常起源
System.out.println("originating in f()");
throw new Exception("throw from f()");
}
public static void g() throws Exception{ //捕获f()中异常后继续抛出
try{
f();
}catch(Exception e){
System.out.println("g() Exception info:");
e.printStackTrace(System.out);
throw e;
}
}
public static void h() throws Exception{ //捕获f()中异常后继续抛出
try{
f();
}catch(Exception e){
System.out.println("h()Exception info:");
e.printStackTrace(System.out);
throw (Exception)e.fillInStackTrace(); //使用fillInStackTrace()更新抛出点信息
}
}
public static void main(String[] args){
try{
g();
}catch(Exception e){
System.out.println("main Exception info:");
e.printStackTrace(System.out);
}
try{
h();
}catch(Exception e){
System.out.println("main Exception info:");
e.printStackTrace(System.out);
}
}
}
我们从运行结果可以看出在使用了fillInStackTrace()后,异常抛出点的信息得到了更细,不再是异常的起源位置。
2.在捕捉异常之后抛出另一种异常。这样有关于原来异常发生点的信息会丢失,剩下的是与新的抛出点有关的信息。
我们可以将上边例子中的h()方法替换为如下方法:
public static void h() throws Exception{
try{
f();
}catch(Exception e){
System.out.println("h()Exception info:");
e.printStackTrace(System.out);
throw new NullPointerException("a new Exception!");//将(Exception)e.fillInStackTrace()替换为新的另一种异常
}
}
我们从运行的结果可以看出在main中是不能发现f()中异常的踪迹。
4.什么是异常链
异常链——在捕获一个异常后抛出另一个异常,并希望把原始异常信息保存下来。
俩种方式:
- Throwable子类中,Error、Exception、TuntimeException三个类中提供了可以传递cause参数的构造器,这样在抛出新异常时可以把原始异常传递进去。(但是之限于这三类异常)
- 其他异常可以通过initCause()方法进行传递。
MyException e=new MyException();
//在抛出新的异常e前将原始的空指针异常保留
e.initCause(new NullPointerException());
throw e;
三、Throwable
- Error——用来表示编译时的系统错误,一般不用关心。
- Exception——是可以抛出的基本类型,我们需要着重关注的。
RuntimeException——如下图所示,其继承自Exception,这种类型的异常往往不需要我们捕获,属于编程错误。它们会自动被Java虚拟机抛出,在程序退出前异常说明会被报告给System.err并会自动调用异常的printStackTrace()方法打印异常信息。
特点:
(1)无法预料的错误,如外部传入的null引用。
(2)在代码中应该进行检查的错误。如数组中的越界异常,往往会导致使用数组的多个位置出现错误。
* 注:*
- 异常不是只有在java.lang包存在的,其在util、net、io包中都有相应的异常。
- 共同点是用名称代表发生的问题,要求名称可以做到望文知意。
- Java代码中可以忽略的异常只有RuntimeException(及其子类)类型的异常,而其他异常都是由编译器强制实施的。
四、异常的限制
1.在构造器或者其他方法中声明将抛出异常后,实际上在代码块中是可抛可不抛的。这样有利于在此方法被覆盖后增加异常,抽象方法同样成立。
//普通方法
public void f() throws Exception{
//代码块中不抛异常
}
//抽象方法
public abstract void g() throws Exception;
2.派生类继承自基类的方法可以不抛出任何异常,即使在基类中定义了异常。因为即使向上转型为基类调用此方法也不会破环程序的真正意图。
class A{
public void f() throws Exception{ } //声明了异常的方法
}
class B extends A{
//重写基类的方法(派生类中重写了方法但是没有抛出异常),这是合理的
public void f(){
System.out.println("this is a overridden method!");
}
}
3.与2中的情况相反,若是在基类中的方法并没有声明异常,那么在派生类中重写此方法是不应该抛出异常的,因为由于需求重写后的方法本应该抛出异常,但是当发生向上转型调用父类此方法时确实不需要抛出异常的,这很显然会使得重写方法的异常机制失灵,不否和程序意图。所以这种情况是不能通过编译的。
class A{
//未声明异常的方法
public void f(){}
}
class B extends A{
//重写父类方法且声明抛出异常——编译不通过
public void f() extends Exception{
}
}
4.异常限制对构造器不起作用,构造器是可以抛出任何异常的,但是在派生类的异常说明中必须包含基类构造器的异常说明。
5.如果一个派生类中重写了既出现在基类中又出现在实现的接口中的方法,那么此方法或者不抛出任何异常,或者不能改变基类中抛出的异常类型。因为如果改变了,就不能判断出实际捕捉到的异常是否是我们真正想要捕捉的异常了。
class A{
public void f() throws ExceptionOne{}
}
interface B{
public void f() throws ExceptionTwo;
}
class C extends A implements B{
//重写方法f()
//可行方案
public void f(){ System.out.print("this is a overridden method!");}
//可行方案
public void f() throws ExceptionOne{ System.out.print("this is a overridden method!"); }
//编译报错
public void f() throws ExceptionTwo{ System.out.print("this is a overridden method!");}
}
注意:
- 一个方法的类型是由其方法名和参数组成的,而异常说明并不属于方法类型的一部分,所以不能基于异常说明来重载方法。
- 出现在基类中的异常说明并不一定要在派生类的异常说明中出现,这说明异常说明的接口会因为继承而变小,这与继承情形是相反的。
五、构造器异常的处理
由于构造器会把对象设置成安全的初始化状态,所以如果在构造器中抛出一些异常,一般的清除行为也许是不能正常工作的,如文件操作的对象,当打开失败会抛出异常,此时需要处理异常,但构造器仍认为这是安全的,此时会出现问题。在Java中我们长用try-catch嵌套模块进行处理。
try{
//创建文件读取对象
InputFile in=new InputFile("file.txt");
try{
//文件读取成功,进行操作
String s;
while((s=in.getLine())!=null){
//……相应操作省略
}
}catch(Exception e){
e.printStackTrace();
}finally{
//操作完毕,关闭文件
in.dispose();
}
}
//构造失败不需要关闭文件,直接进行异常处理
catch(Exception e){
System.out.print("InputFlie construction failed!");
}
经过如上操作就会避免在文件打开失败后仍进行文件关闭操作。
---------------------------------------
异常处理机制这一块看的时间比较长,感觉还是不能真正感受到Java异常机制的精髓。我们在实际开发中往往总是觉得自己的程序没有问题,所以在创建一个方法时是否要创建一个新的异常,什么时候应该给方法声明一个异常需要我们在设计时深思熟虑。异常机制可以帮助我们许多,但用不好又会给我们添加许多不必要的麻烦,所以使用需慎重。自我感觉还是应该多看一下人家成熟的方法库是如何恰当定义并使用异常的,真正去体会还是要在实战中。如果有读者对异常这一块有独到的见解可以一起讨论学习。