java异常处理机制
我们都知道,java异常处理机制对于我们java程序员来说是非常重要的东西,因为有了这个机制,我们不需要像以前一样,当异常发生的时候,只能眼巴巴的看着我们的程序停止工作或者是必须在每一句可能产生异常代码的后面添加处理异常的代码,以至于我们的代码变得越来越难以阅读。就因为这点,在过去,很多程序员都是选择忽视可能产生异常的地方,将这个处理直接交给用户程序员,反正我已经搞定我的工作,至于这个程序是否会产生异常,就你自己看着办,就算发生异常,也是你自己处理。就是因为这种态度,所以才有一些遗留下来的丑陋的代码集合体!但是我们又不能过多责怪这些人,因为他们也是受害者,有谁想要只是为了防止可能出现或者可能不出现的异常就要写一大堆代码!就算是程序员,不,就因为是程序员,所以,才更加会想要偷懒!
也就是因为我们这下想要偷懒的程序员,所以才会出现异常处理机制这种方便的东西。那么,我们接下来就是要着重介绍,异常处理机制这种东西到底能够为我们带来多少方便。
那么,第一个问题,就是什么是异常?所谓的异常,就是程序中出现的阻止当前方法或作用域继续执行下去的问题。所以,要是没有异常处理机制,我们的程序就只能崩溃而无法执行下去。那么,我们如何建立异常处理机制呢?要想建立异常处理机制,首先要知道异常处理机制的原理。异常处理机制到底是怎么工作的呢?其实很简单,如果你手上有一个即将爆炸的炸弹,你会怎么做?大部分人的答案都是一样的,就是马上丢掉!是的,就是丢掉,异常处理机制也跟一般人一样,选择将这个异常抛出当前方法的执行点。那么,问题来了,到底它将异常抛到哪里?就是抛到上一级的环境,就是处理异常的地方。这里我们需要注意,就是异常这个东西到底为什么能够被异常处理机制捕获?这个问题就是异常处理机制的核心,了解它,你就能明白为什么异常处理机制会是这样子。首先,异常处理机制会使用new在堆上创建异常对象,然后当前的执行路径被终止,并且从当前环境中弹出对异常对象的引用,是的,就是将这个异常对象的引用传给接下来的异常处理程序,这就像是一个方法调用一样,事实上,也正是如此。既然会创建异常对象,那么,这个对象的销毁呢?只要是稍微使用过java的人都知道,这个问题根本就不需要担心,因为有垃圾回收器,既然异常对象是在堆上建立的,那么垃圾回收器知道什么时候回收它,而且总会回收它的(虽然不知道什么时候回收而已)。那么,理解了异常处理机制的原理,我们来看看异常处理机制到底是怎样建立的?建立异常处理机制的一般代码如下:
try{ .... }catch(Exception e){ e.printStackTrace(); }
看到上面的代码,我们要清楚两点,就是try和catch里面处理的分别是什么?首先,try里面的代码是可能产生异常的地方,而catch的参数就是异常的类型,里面就是异常的处理了。通过这样的代码,我们就能捕获异常并且“正确”的处理异常,是的,这里的确是引号,因为大部分的异常的确是可以得到正确的处理,但是有些异常我们呢是连类型是什么都不知道的,所以也就根本谈不上什么正确的处理了。但是,请放心,就大部分情况来说,这种处理还是比较妥当的,就算我们无法处理异常,还是可以像我上面写的那样,只是输出栈的信息也可以,至少,我们的代码可以运行到下面而不会中断。
java的标准库提供了大量的异常类型,如我们常见的NullPointer等等,但是有时候我们是需要自定义异常的,就像前面说过的,有些异常是我们无法确切知道类型的,那么我们就要自定义这个异常并且做出处理。如何自定义异常呢?就像继承一样,异常也有一个基类,就是Exception,它可以捕捉所有的异常,所以我们的自定义异常只要继承这个基类就能被异常处理机制捕捉到,如:
class myException extends Exception{}
当然,你也可以继承更加详细的异常类型,但一般这就足够了,因为我们的自定义异常只要能被异常处理机制捕捉到就行。
既然讲到Exception类,就必须提醒各位,如果你的异常处理机制中有处理Exception和其他从它继承而来的异常(所有的异常都是从它继承而来),那么,你的Exception必须放在最后,因为异常处理机制是有所谓的异常匹配的问题,它会寻找最近的与它匹配的异常,然后进入相关的处理中,所以,Exception是要放在最后,否则,会屏蔽掉其他具体的异常。这同样适用于派生的异常类和它的基类,而且,派生的异常类也是可以被基类的处理程序捕获的。
当然,既然是有关于异常的问题,尤其是程序的开发,总是会有所谓的异常日志的,就像android的日志一样,方便我们查看程序中的异常产生过程。那么,如何将异常记录到日志中呢?先来看一下相关的代码:
class LoggingException extends Exception{ private static Logger logger = Logger.getLogger("LoggingException"); public LoggingException(){ StringWriter trace = new StringWriter(); printStackTrace(new PrintWriter(trace)); logger.severe(trace.toString()); } }
让我们来看看这个代码。为什么Logger是静态的呢?我们都知道,所谓的静态数据是可以不需经由创建对象就可以直接使用的,就像下面的构造器那样,直接将调用logger上的severe()方法。如果在构造器中还需要创建对象的话,那就真的是开玩笑了!!好吧,不说这个了,我们来看看这个Logger对象。有关于Logger对象是有很多东西可讲的,但是说真的,实在没有必要,因为那些东西我这里说不清楚,而且不需要了解太多,只需知道,就是这个Logger会将异常的信息,就是后面的String输出发送到System.err上。至于System.err是什么,我就没必要说了,只要查一下就行。注意的是severe()这个方法。这个方法会记录一条SEVERE消息,而所谓的SEVERE消息,就是我们平常在日志中看到的消息(好吧,这一块我讲得实在是太烂了,因为我对这方面实在是有点陌生,等以后我摸熟之后会跟大家说的,如果这里有什错误的话,请告诉我)前面只是讲了如何将异常的信息写进日志中,但是我们如何让它显示出来呢?这一般是在异常处理程序中,如下面的代码:
public class LoggingException{ private static Logger logger = Logger.getLogger("LoggingException"); static void logException(Exception e){ StringWriter trace = new StringWriter(); e.printStackTrace(new PrintWriter(trace)); logger.severe(trace.toString()); } } public static void main(String[] args){ try{ throw new NullPointerException(); }catch(NullPointerException e){ logException(e); } }
这里就很好理解了,我们在代码中抛出异常,然后捕获该异常,并且输出信息到日志中。这同样能够用来捕获和记录别人编写的异常。
前面的内容已经将异常处理机制的基本内容都讲完了,是的,本来这部分就没有多少内容好讲,就是要求我们记住一个套路而已。那么,是否说明我的文章已经结束了呢?答案肯定是绝对不是的!按照我的习惯,当我接触一种东西的时候,一定会想要知道这东西是什么,怎么用,用的时候会出现什么问题,怎么解决等等许多附加的子问题,当然,异常这里也不例外。先来讲讲异常中一种比较特殊的类型,RunTimeException。
为什么说它特殊呢?因为这种异常是可以被忽略的!没错,你没有看错,就是忽略。这当然很奇怪,异常为什么可以忽略呢?前面不是讲过吗,异常是必须要处理的,否则就会使得程序无法继续下去,如果我们忽略异常,我们的程序要怎么办?别急,这是因为这种异常与其他异常不一样,这种异常的出现说明我们的代码中出现编程错误。什么是编程错误?一般有两种情况:
1.无法预料到的错误,比如从控制范围外传进的null引用,这种错误根本就无法处理,因为你根本就不知道会发生这种错误,它的发生完全是偶然的,是因为你在写代码的时候引进的;
2.应该在代码中进行检查的错误,如数组的越界问题。这种问题就真的是你自己的疏忽而造成的,凭什么我们这些设计者就要为你这种脑残负责呢?
所以,RunTimeException是可以忽略的,就是因为它在很大程度上就是你自己编程的问题,是必须你自己去负责的。
接下来就是有关于finally子句的问题。finally子句在try-catch块中是很常见的,因为它能够帮我们做一些东西,比如说清理。是的,因为finally子句的特点就是无论try块中的异常是否抛出,都能得到执行,就算你在try块里面使用return都一样。。有时候我们是很难正确的关掉我们的资源的。比如说,如:
try{ InputFile in = new InputFile(...); }catch(Exception e){...}
你怎么关掉你的资源?这里如果你在try块里最后加上这一句:in.close(),那么,当出现异常的时候,你的代码就会脱离try块,那么,关闭这个动作就不会发生!所以,这时就需要用到finally子句,如:
try{ InputFile in = new InputFile(...); }catch(Exception e){ ...}finally{ in.close(); }
这样,我们就能确保,无论try块是否会抛出异常,我们都能正确的关闭资源。
但是,就是因为这个,会产生很多问题。某些情况下,finally子句里的代码也是会产生异常,这时,在finally子句里抛出的异常就会将原本的异常替换掉,使得我们丢失异常。还有,如果在finally子句中使用return,就真的是return了,因为它是一定会执行的。丢失异常信息是异常处理机制中一个隐秘的问题,甚至有些程序员根本就没有发觉这个问题,这是我接下来将要讲的问题,但是,现在,先就上面的问题继续。
对于上面的第一种情况(第二种你只要不在里面return就行),我们的解决方案就是嵌套try的使用,就是在finally子句里再使用try-catch来捕获并处理异常。嵌套的try-catch的用处很多,比如说,有时候一个对象的创建也是会出现异常的,这时候怎么办呢?如:
try{ InputFile in = new InputFile(...); try{ ... }catch(Exception e){ ... }finally{in.close();} }catch(Exception e){ ... }
看到没,如果in的构建出现问题,就会有相关的异常处理接管,然后是用finally子句来正确的关闭该资源。
接下来,就是关于异常丢失的问题。在java中,只要你这个代码可能会抛出异常,它就会强制要求你使用异常处理机制,但是有时候我们根本就没有想过要如何处理异常啊!本来正确的异常处理机制应该是当你知道如何处理异常的时候使用,但是java已经将它作为强制性的设定了。这样就会出现问题,我并不知道如何处理异常,当是我的代码又必须通过编译,所以,我只能将就的编写不正确的处理程序,那么,当我的代码通过编译后,很可能我已经遗忘了这个异常,因为没有任何提示啊!那么,这个本来应该马上被处理的异常就会永远留在我的代码里但是我本人永远不知道。所以,一般遇到这种情况,我们都是选择打印栈信息来作为处理,以便以后能够解决的时候再解决。或者还有一个经常看到的做法,就是将异常传递给控制台。如:
public static void main(String[] args) throw Exception{ ... }
这样我们就可以不必在main()里使用try-catch了,我们的程序在运行时就会看到在控制台里会有相关的的异常信息打印出来,我们就不用害怕我们会丢失异常。但是这个问题并不是通用的,因为在处理文件的输入/输出时,它就显然不够用了,因为那些情况真的很复杂!!!
于是我们呢还有其他方法来避免这种问题,就是异常链。如:
try{ .... }catch(Exception e){ throw new RunTimeException(e); }
这里必须说明一下异常链。当我们在异常后面添加一个异常作为参数的时候,就是作为该异常的构造器的参数时,它就会像链表一样,将作为参数的异常放在这个异常后面。这种做法就是为了保证我们不会丢失任何原始异常的信息,因为它们最终都会一起显示出来。但是,只有三种异常类型具有自带的异常链功能,就是Error, Exception, RunTimeException。其他的异常你必须使用initCause()方法。这种方法使得我们不用写try-catch块或者异常说明,直接忽略异常,让它们自己从调用栈中冒出来,而且我们还可以使用getCause()捕获并处理特定的异常,但是,记得,被承载的异常是要放在最后面处理的,理由和基类异常和派生类异常一样。当然,我们也可以创建RunTimeException的子类,样我们的自由度更高。
使用异常时,也是具有一些限制的。比如说,覆盖方法时,我们只能抛出在基类方法中的异常说明里列出的异常,为什么呢?因为这样子是为了让基类使用的代码应用到其派生类对象上时,一样能够工作,如果派生类没有它相应的异常说明,那么,显然该方法是无法正常工作的,而且派生类向上转型为基类时,异常说明并没有跟着向上转型,所以这时也会出现问题。还有,当一个类既实现一个接口又继承一个基类,那么异常说明是用基类的,也是同样的道理。但是,异常这种限制对于构造器来说病不起作用,派生类的构造器可以抛出任何异常,但是,习惯上,派生类的构造器的异常说明必须包含基类构造器的异常说明,因为基类构造器总是会被调用的,如默认构造器的自动调用,但是,派生类构造器不能捕获基类构造器抛出的异常,这里是指我们在调用基类构造器时抛出的异常,这时,怎么办呢?只能使用try-catch块了。
派生类方法可以不抛出任何异常,因为即使基类的方法会抛出异常,但是也不会破坏已有的程序,因为这个基类方法本身就已经被覆盖了!基类的异常是不会影响到我这个派生类方法的使用的,就像已经分家的孩子和父母,基本上,父母家里的事是不会影响到孩子家的。所以,比起接口的继承来说,异常的继承是变小了而不是变大了。
这里,我们来谈谈为什么main线程无法捕获添加的线程抛出的异常,是因为添加的线程本身也是具有相关的异常处理机制,哪怕它没有,但是也会跑到控制台里显示出来,所以,main线程是无法捕获的。那么,我们是如何对每个线程都添加异常处理器呢?我们可以修改Executor的生产方式,在每个Thread对象上都附加一个异常处理器,所以我们首先要实现的是一个Thread.UnCaughtExceptionHandler接口,如:
class A implements Thread.UnCaughtExceptionHandler{ public void uncaughtException(Thread t, Throwable e){ ... } }
然后将这个异常处理器附加到Thread对象上,如:Thread t = new Thread(RunnableClass);t.setUnCaughtExceptionHandler(new A()).是的,我们的确是可以按照情况逐个设置每个线程的异常处理器,但是我们更多的情况却是我们的异常处理器其实都是一样的,那么,我们就可以在我们的Thread类中设置一个静态域,并将这个处理器设置为默认的,像是这样做,Thread.setUnCaughtExceptionHandler(new A());这个只有在线程没有专有的异常处理器的时候才会被调用。一般我们的编译器是这样处理的:显示检查每个线程是否有其专门的异常处理器,如果没有,再看看这个Thread类(线程组)是否有这个线程组专有的(就是看看这些放在一块的线程们),如果还是没有,再调用系统默认的。
我们经常可以看到这一句话:“异常直达main()而没有被捕获。”这句话是什么意思呢?如:
void f(){ try{ ... }catch(Exception e){ ... } } void g(){ try{ f(); }catch(Exception e){ ...} } public static void main(String[] args){ g(); }
我们看一下,在main()中调用g(),g()中调用f(),如果f()中的异常没有被处理,那么就会跑到g(),g()中又没有处理这个异常,那么就会跑到main()中,这就是这句话所要表达的过程。
最后,就是罗列一下异常的使用场合作为本文的结尾,希望本文能对那些想要了解异常的朋友有所帮助:
1.在恰当的级别处理问题(在知道如何处理的情况下才捕获异常);
2.解决问题并且重新调用产生异常的方法;
3.进行少许修补,然后绕过异常发生的地方继续执行;
4.用别的数据进行计算,以代替方法预计会返回的值;
5.把当前运行环境下能做的事尽量做完,然后把相同的异常重抛到更高层;
6.把当前运行环境下能做的事情尽量做完,然后把不同的异常抛到更高层;
7.终止程序;
8.进行简化,如果你的异常模式是问题变得太复杂;
9.让类库和程序更加安全。