当前位置: 首页 > 科技观察

说说Java异常处理基础

时间:2023-03-20 12:28:52 科技观察

本文转载自微信公众号“蜗牛上网”,作者:白蜗牛。转载本文请联系蜗牛网公众号。看完这篇文章,您将了解到:什么是不正常我们在日常生活中经常会遇到一些意想不到的事情,比如坐火车没有带身份证,那么就无法顺利上车。计算机世界也有类似的情况。术语是Exception,其实是ExceptionEvent的缩写。异常是程序执行过程中发生的、中断程序正常运行的事件。比如你没有带身份证上车,这是一个异常事件,会影响你正常上车。计算机程序运行会有一个主入口,一般我们称之为main方法,在main方法内部可能会调用其他各种方法。当方法中发生错误时,该方法创建一个对象并将其交给运行时系统。该对象称为异常对象,它包含与错误相关的信息,包括错误类型和程序状态。创建异常对象并将其传递给运行时系统称为抛出异常。当方法抛出异常时,运行时系统会尝试找到处理异常的方法。首先系统会判断错误的方法是否已经处理。如果没有,就会把异常抛给上层方法,直到找到有异常处理的方法。这样,从发生错误的方法到处理异常的方法,就会形成一个有序的调用方法列表。这个方法列表称为调用堆栈。应用程序的每个方法都会按照调用的先后顺序入栈,栈是先进后出的。例如main方法进栈开始执行程序。当调用其他方法时,其他方法也被压入堆栈。等其他方法执行完,其他方法出栈,继续执行main方法,main方法执行完出栈,栈为空,程序结束。运行时系统在调用堆栈中查找包含可以处理异常的代码块的方法。这段代码称为异常处理程序。通过调用栈,从出错的方法开始,按照方法调用的逆序查找(栈具有先进后出的特点)。当找到合适的异常处理程序时,运行时系统将异常传递给处理程序。如果抛出的异常对象的类型与处理程序可以处理的类型匹配,则异常处理程序被认为是合适的。选择异常处理程序的过程称为捕获异常。如果运行时系统搜索了调用堆栈上的所有方法,仍然找不到合适的异常处理程序,则运行时系统(以及随后的程序)将终止。观察下面的代码,想一想它是如何工作的?packagecom.springtest.demo;publicclassTest{/***程序主方法**@paramargs程序入参*/publicstaticvoidmain(String[]args){//用户输入字符串woniuStringwoniu="woniu";intnum=str2number(woniu);System.out.println(num);}/***strtointeger**@paramstrstring*@returninteger*/privatestaticintstr2number(Stringstr){//解析成数字,抛出NumberFormatExceptionreturnInteger.parseInt(str);}}输出是这样的:Exceptioninthread"main"java.lang.NumberFormatException:Forinputstring:"woniu"atjava.lang.NumberFormatException.forInputString(NumberFormatException.java:65)atjava.lang.Integer.parseInt(Integer.java:580)atjava.lang.Integer。parseInt(Integer.java:615)atcom.springtest.demo.Test.str2number(Test.java:29)atcom.springtest.demo.Test.main(Test.java:15)观察运行的结果信息,我们发现应用程序主程序异常,程序终止,因为没有打印num的值。结果也告诉我们发生了NumberFormatException,即数字格式异常,后面也提示字符串woniu不能转换数字。这符合我们的预期。然后是调用堆栈。调用栈中的每一行信息表示异常流转过程中的方法路径、类名和代码行数。第一行信息是最先出现异常的地方,也可以作为我们排查问题的依据。很明显,forInputString抛出异常后,parseInt和str2number都只是转发异常,并没有捕获异常,即使在main方法中,也没有捕获异常。最后,因为没有异常处理器,程序执行终止。如何捕获和处理异常为了让程序正常运行而不被意外终止,Java编程规范要求必须捕获或指定异常。使用try捕获异常的第一步是使用try将可能抛出异常的代码括起来。语法如下:try{//可能导致异常的代码}try包含一个代码块,可以把可能导致异常的代码放在里面。代码可以是一行或多行。这也意味着这段代码可能会引发许多不同类型的异常。只有try的异常处理程序不会编译。如果使用javac命令编译一个只有try的java文件,会报如下错误:Error:'try'without'catch','finally'orresourcedeclarationerror:'try'without'catch','finally'orTheresourcedeclarationtry{^1haserrors,所以try代码块只是划定了捕获异常的范围,仅仅依靠try进行异常管理显然是不够的。使用catchcatch语法,捕获异常还需要第二步:使用catch捕获异常并进行异常处理。语法如下:try{//可能引发异常的代码}catch(ExceptionType1name1){//异常类型1ExceptionType1命中时的异常处理代码}catch(ExceptionType2name2){//异常时的异常处理代码type2ExceptionType2是hit}catch是搭配try使用的,不会单独出现。Try后面可以跟多个catch代码块来处理try中出现的各种类型的异常。每个catch代码块都是一个异常处理器,异常类型在处理时由catch参数指定。在catch的括号中,参数ExceptionType声明了这个handler可以处理的异常类型。该异常类型必须是从Throwable类继承的类。Java异常的继承体系说到Throwable,就不得不说说Java的异常体系。下面是Java异常的继承层次图。Throwable是异常系统的根,继承自Object。Throwable分为两个系统:Error和Exception。Error表示严重的错误,一般是程序无法处理的,比如StackOverflowError表示栈溢出。Exception表示一个运行时错误,可以被捕获和处理。Exception可以分为两类:RuntimeException和CheckedException。RuntimeException是指运行时异常,是由于程序逻辑的编程不正确导致的,例如NullPointerException表示空指针异常,IndexOutOfBoundsException表示数组索引越界。这种异常是代码错误,应该修复程序代码。int[]arrry={0,1,2};//这里会抛出java.lang.ArrayIndexOutOfBoundsException,arry[3]之类的代码不应该出现System.out.println(arrry[3]);CheckedException表示A检查异常,这是程序逻辑的一部分。例如,IO异常的IOException和未找到文件的FileNotFoundException。这个异常必须被捕获并处理,否则编译会失败。以下代码无法编译:publicclassA{publicstaticvoidmain(String[]args){FileInputStreaminputStream=newFileInputStream("/");}}javac编译会报如下错误,还会提示使用try/catch捕获或添加exceptions在语句中抛出很方便。错误:未报告的异常错误FileNotFoundException;必须捕获或声明抛出FileInputStreaminputStream=newFileInputStream("/");^1errorscatch回用catch语法,ExceptionType对应Java异常系统类或其子类中的Exception。name是给异常类型的名称,花括号中的内容是调用异常处理器时执行的代码。此处的代码可以通过名称引用异常。当调用栈中出现异常时,运行时系统将调用异常处理器。当异常处理程序的ExceptionType与异常的类型匹配时,即命中某个catch块,异常对象将被赋值给异常处理程序的参数。然后执行catch块的异常处理代码。我们可以在异常处理程序中做很多事情,比如打印错误日志、暂停程序、执行错误恢复、提示用户或者将异常传递给上层。以下是打印错误信息的示例代码:publicstaticvoidmain(String[]args){try{int[]arrry={0,1,2};//这里会抛出java.lang.ArrayIndexOutOfBoundsException,并且arry[3不应该出现]这样的代码System.out.println(e);}}输出结果为:Anarrayout-of-boundsexceptionwascatched:java.lang.ArrayIndexOutOfBoundsException:3在某些场景下,我们的一段代码可能会引发多个异常,而异常处理会更加一致,比如打印日志。这种情况下,如果都单独设置一个catch块,写同样的代码,重复率很高。所以在Java7之后,一个catch块支持处理多种类型的异常。语法如下:try{//可能引发异常的代码}catch(ExceptionType1name1|ExceptionType2name2){//命中异常类型1ExceptionType1或异常类型2ExceptionType2时的异常处理代码}使用finally程序有时会打开一些资源,当它正在运行,比如文件、连接、线程等等。如果在程序运行过程中抛出异常,程序终止,打开的资源将永远不会被释放,这将导致资源泄漏甚至系统崩溃。再比如,在程序运行结束前,我想输出一个摘要日志用于监控,但是如果程序中途抛出异常导致程序终止,则不会打印日志,看不到信息我想。因此需要一种机制,在异常发生,进程被阻塞时,能够释放开放的资源或者执行指定的逻辑。Java使用finally来达到这个目的,finally可以形成try-finally结构或者try-catch-finally结构。但是finally块总是在try退出时执行。这个“一直”可以分为以下几种情况:没有执行异常try,没有异常发生,然后执行finally代码块,和正常程序一样顺序执行。publicstaticvoidmain(String[]args){System.out.println("main:"+fetchMyName());}publicstaticStringfetchMyName(){Stringme="woniu";try{me="woniu666";}finally{System.out.println("finally:"+me);}returnme;}输出:finally:woniu666main:woniu666有一个异常没有被捕获。如果在try执行过程中出现异常,会抛出异常对象,但finally代码块还是会执行。publicstaticvoidmain(String[]args){System.out.println("main:"+fetchMyName());}publicstaticStringfetchMyName(){Stringme="woniu";int[]arrry={0,1,2};尝试{me="woniu666";//这里会抛出java.lang.ArrayIndexOutOfBoundsException,arry[3]这样的代码不应该出现System.out.println(arrry[3]);}finally{System.out.println("finally:"+me);}returnme;}fetchMyName()如果没有捕获到异常,会向上抛出,但是会先执行finally中的逻辑,不会在main方法中捕获到异常,因此程序将被阻塞,从而打印出调用堆栈。finally:woniu666Exceptioninthread"main"java.lang.ArrayIndexOutOfBoundsException:3atcom.springtest.demo.TryFinally.fetchMyName(TryFinally.java:28)atcom.springtest.demo.TryFinally.main(TryFinally.java:15)出现异常是一个capturetryexecution,如果过程中出现异常,会抛出异常对象,catch会捕获异常并正常处理。这个时候finally代码块还是会被执行。publicstaticvoidmain(String[]args){System.out.println("main:"+fetchMyName());}publicstaticStringfetchMyName(){Stringme="woniu";int[]arrry={0,1,2};尝试{me="woniu666";//这里会抛出java.lang.ArrayIndexOutOfBoundsException,arry[3]这样的代码不应该出现System.out.println(arrry[3]);}catch(ArrayIndexOutOfBoundsExceptione){System.出去。println("命中数组索引越界异常的处理器,越界索引为:"+e.getMessage());}finally{System.out.println("finally:"+me);}returnme;}代码运行正常,先执行catch代码块中的逻辑,然后执行finally代码块,最后执行main方法。命中数组索引越界异常的处理器,越界索引为:3finally:woniu666main:woniu666tryreturn中return表示方法执行结束,finally在try退出时执行,所以如果return包含在try代码块,finally代码块仍然会被执行到达?尝试在代码块中添加一个返回!publicstaticvoidmain(String[]args){System.out.println("main:"+fetchMyName());}publicstaticStringfetchMyName(){Stringme="woniu";int[]arry={0,1,2};try{me="woniu666";//这里没有抛出异常System.out.println(arrry[0]);return"try";}catch(ArrayIndexOutOfBoundsExceptione){System.out.println("命中数组索引越界异常的处理器,越界索引为:"+e.getMessage());}finally{System.out.println("finally:"+me);}returnme;}看到结果还是会去执行finally代码块!0finally:woniu666main:returninreturntryintrycatch我们试过了,那么return和finallyincatch是怎么执行的呢?publicstaticvoidmain(String[]args){System.out.println("main:"+fetchMyName());}publicstaticStringfetchMyName(){Stringme="woniu";int[]arrry={0,1,2};try{me="woniu666";//这里会抛出java.lang.ArrayIndexOutOfBoundsException,不应该出现像arry[3]System.out.println(arrry[3]);}catch(ArrayIndexOutOfBoundsExceptione){System.out.println("命中数组索引越界异常的处理器,并且索引越界是:“+e.getMessage());return"catch";}finally{System.out.println("finally:"+me);}returnme;}结果还是会去执行finally代码块!命中数组索引越界异常的处理器,越界索引为:3finally:woniu666main:catch介绍了如何指定方法抛出异常的知识后,可以想象另一种情况,即就是,当前方法抛出异常后,但是当前方法不适合处理这个异常,而调用栈的上层其实当前方法最好不要捕获异常,可以让方法上调用堆栈的上层来处理它。这时,如果抛出的异常是checkedexception,那么你必须在方法上指定它可以抛出这些异常。您需要在方法声明中添加一个throws语句。throws语句包含throws关键字,后跟该方法抛出的所有异常,以逗号分隔。throws语句放在方法名和参数列表之后,以及定义方法范围的括号之前。代码示例如下:publicstaticvoidtest()throwsFileNotFoundException{FileInputStreaminputStream=newFileInputStream("/");}被上层main方法捕获处理:publicstaticvoidmain(String[]args){try{test();}catch(FileNotFoundExceptione){System.out.println("Filenotfoundexception:"+e.getMessage());}}可以正常输出:filenotfoundexception:/(Isadirectory)据说checked异常必须处理,因为如果不处理,compilationwillfailPass,要么捕获并处理异常,要么指定方法抛出的异常。uncheckedexception,即runtimeexception是否也有这个要求?uncheckedexceptions不是强制的,可以指定方法抛出的异常,也可以不指定。如果不指定,异常对象将沿着调用栈不断向上抛出,直到被捕获并处理或程序终止。总结本文介绍异常的概念。我们了解了异常相关的术语、异常产生的背景、异常的运行机制。然后我们介绍了如何捕获异常以及如何按照Java编程规范来指定异常。同时,我们也引入了Java异常。继承系统。这些都是很基础的内容,但是也很重要。写代码的时候一定要考虑这方面。你甚至可以认为面向异常编程是对你编码能力的考验。